Skip to content

Commit 8fc1f6e

Browse files
authored
feat(hub): adding pathsInfo function (#1031)
## Description Following discussion #1024 and incompatibility of using the `HEAD` request to get the same etag as the python library is using for populating the cache directory. This PR add the `pathsInfo` function that return the paths information including the LFS oid (or etag) if the file is a LFS pointer. As suggested by @coyotte508 in #1024 (review) ## Related issues Fixes #1023 (provide an alternative method to `fileDownloadInfo`. ## Tests - [x] unit tests has been added
1 parent 0db316c commit 8fc1f6e

File tree

3 files changed

+196
-0
lines changed

3 files changed

+196
-0
lines changed

packages/hub/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from "./model-info";
1919
export * from "./oauth-handle-redirect";
2020
export * from "./oauth-login-url";
2121
export * from "./parse-safetensors-metadata";
22+
export * from "./paths-info";
2223
export * from "./space-info";
2324
export * from "./upload-file";
2425
export * from "./upload-files";
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { expect, it, describe } from "vitest";
2+
import type { CommitInfo, PathInfo, SecurityFileStatus } from "./paths-info";
3+
import { pathsInfo } from "./paths-info";
4+
5+
describe("pathsInfo", () => {
6+
it("should fetch LFS path info", async () => {
7+
const result: PathInfo[] = await pathsInfo({
8+
repo: {
9+
name: "bert-base-uncased",
10+
type: "model",
11+
},
12+
paths: ["tf_model.h5"],
13+
revision: "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7",
14+
});
15+
16+
expect(result).toHaveLength(1);
17+
18+
const modelPathInfo = result[0];
19+
expect(modelPathInfo.path).toBe('tf_model.h5');
20+
expect(modelPathInfo.type).toBe('file');
21+
// lfs pointer, therefore lfs should be defined
22+
expect(modelPathInfo?.lfs).toBeDefined();
23+
expect(modelPathInfo?.lfs?.oid).toBe("a7a17d6d844b5de815ccab5f42cad6d24496db3850a2a43d8258221018ce87d2");
24+
expect(modelPathInfo?.lfs?.size).toBe(536063208);
25+
expect(modelPathInfo?.lfs?.pointerSize).toBe(134);
26+
27+
// should not include expand info
28+
expect(modelPathInfo.lastCommit).toBeUndefined();
29+
expect(modelPathInfo.securityFileStatus).toBeUndefined();
30+
});
31+
32+
it("expand parmas should fetch lastCommit and securityFileStatus", async () => {
33+
const result: (PathInfo & {
34+
lastCommit: CommitInfo,
35+
securityFileStatus: SecurityFileStatus,
36+
})[] = await pathsInfo({
37+
repo: {
38+
name: "bert-base-uncased",
39+
type: "model",
40+
},
41+
paths: ["tf_model.h5"],
42+
revision: "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7",
43+
expand: true, // include
44+
});
45+
46+
expect(result).toHaveLength(1);
47+
48+
const modelPathInfo = result[0];
49+
50+
// should include expand info
51+
expect(modelPathInfo.lastCommit).toBeDefined();
52+
expect(modelPathInfo.securityFileStatus).toBeDefined();
53+
54+
expect(modelPathInfo.lastCommit.id).toBe("dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7");
55+
expect(modelPathInfo.lastCommit.title).toBe("Update tf_model.h5");
56+
expect(modelPathInfo.lastCommit.date.getTime()).toBe(1569268124000); // 2019-09-23T19:48:44.000Z
57+
});
58+
59+
it("non-LFS pointer should have lfs undefined", async () => {
60+
const result: (PathInfo)[] = await pathsInfo({
61+
repo: {
62+
name: "bert-base-uncased",
63+
type: "model",
64+
},
65+
paths: ["config.json"],
66+
revision: "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7",
67+
});
68+
69+
expect(result).toHaveLength(1);
70+
71+
const modelPathInfo = result[0];
72+
expect(modelPathInfo.path).toBe("config.json");
73+
expect(modelPathInfo.lfs).toBeUndefined();
74+
});
75+
});

packages/hub/src/lib/paths-info.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { CredentialsParams, RepoDesignation } from "../types/public";
2+
import { checkCredentials } from "../utils/checkCredentials";
3+
import { toRepoId } from "../utils/toRepoId";
4+
import { HUB_URL } from "../consts";
5+
import { createApiError } from "../error";
6+
7+
export interface LfsPathInfo {
8+
"oid": string,
9+
"size": number,
10+
"pointerSize": number
11+
}
12+
13+
export interface CommitInfo {
14+
"id": string,
15+
"title": string,
16+
"date": Date,
17+
}
18+
19+
export interface SecurityFileStatus {
20+
"status": string,
21+
}
22+
23+
export interface PathInfo {
24+
path: string,
25+
type: string,
26+
oid: string,
27+
size: number,
28+
/**
29+
* Only defined when path is LFS pointer
30+
*/
31+
lfs?: LfsPathInfo,
32+
lastCommit?: CommitInfo,
33+
securityFileStatus?: SecurityFileStatus
34+
}
35+
36+
// Define the overloaded signatures
37+
export function pathsInfo(
38+
params: {
39+
repo: RepoDesignation;
40+
paths: string[];
41+
expand: true; // if expand true
42+
revision?: string;
43+
hubUrl?: string;
44+
/**
45+
* Custom fetch function to use instead of the default one, for example to use a proxy or edit headers.
46+
*/
47+
fetch?: typeof fetch;
48+
} & Partial<CredentialsParams>
49+
): Promise<(PathInfo & {lastCommit: CommitInfo, securityFileStatus: SecurityFileStatus })[]>;
50+
export function pathsInfo(
51+
params: {
52+
repo: RepoDesignation;
53+
paths: string[];
54+
expand?: boolean;
55+
revision?: string;
56+
hubUrl?: string;
57+
/**
58+
* Custom fetch function to use instead of the default one, for example to use a proxy or edit headers.
59+
*/
60+
fetch?: typeof fetch;
61+
} & Partial<CredentialsParams>
62+
): Promise<(PathInfo)[]>;
63+
64+
export async function pathsInfo(
65+
params: {
66+
repo: RepoDesignation;
67+
paths: string[];
68+
expand?: boolean;
69+
revision?: string;
70+
hubUrl?: string;
71+
/**
72+
* Custom fetch function to use instead of the default one, for example to use a proxy or edit headers.
73+
*/
74+
fetch?: typeof fetch;
75+
} & Partial<CredentialsParams>
76+
): Promise<PathInfo[]> {
77+
const accessToken = checkCredentials(params);
78+
const repoId = toRepoId(params.repo);
79+
80+
const hubUrl = params.hubUrl ?? HUB_URL;
81+
82+
const url = `${hubUrl}/api/${repoId.type}s/${repoId.name}/paths-info/${encodeURIComponent(params.revision ?? "main")}`;
83+
84+
const resp = await (params.fetch ?? fetch)(url, {
85+
method: "POST",
86+
headers: {
87+
...(params.credentials && {
88+
Authorization: `Bearer ${accessToken}`,
89+
}),
90+
'Accept': 'application/json',
91+
'Content-Type': 'application/json'
92+
},
93+
body: JSON.stringify({
94+
paths: params.paths,
95+
expand: params.expand,
96+
}),
97+
});
98+
99+
if (!resp.ok) {
100+
throw await createApiError(resp);
101+
}
102+
103+
const json: unknown = await resp.json();
104+
if(!Array.isArray(json)) throw new Error('malformed response: expected array');
105+
106+
return json.map((item: PathInfo) => ({
107+
path: item.path,
108+
lfs: item.lfs,
109+
type: item.type,
110+
oid: item.oid,
111+
size: item.size,
112+
// expand fields
113+
securityFileStatus: item.securityFileStatus,
114+
lastCommit: item.lastCommit ? {
115+
date: new Date(item.lastCommit.date),
116+
title: item.lastCommit.title,
117+
id: item.lastCommit.id,
118+
}: undefined,
119+
}));
120+
}

0 commit comments

Comments
 (0)