@@ -4,14 +4,58 @@ import type { CredentialsParams, RepoDesignation } from "../types/public";
44import { checkCredentials } from "../utils/checkCredentials" ;
55import { toRepoId } from "../utils/toRepoId" ;
66
7+ const HUGGINGFACE_HEADER_X_REPO_COMMIT = "X-Repo-Commit"
8+ const HUGGINGFACE_HEADER_X_LINKED_ETAG = "X-Linked-Etag"
9+ const HUGGINGFACE_HEADER_X_LINKED_SIZE = "X-Linked-Size"
10+
711export interface FileDownloadInfoOutput {
812 size : number ;
913 etag : string ;
14+ commitHash : string | null ;
1015 /**
1116 * In case of LFS file, link to download directly from cloud provider
1217 */
1318 downloadLink : string | null ;
1419}
20+
21+ /**
22+ * Useful when we want to follow a redirection to a renamed repository without following redirection to a CDN.
23+ * If a Location header is `/hello` we should follow the relative direct
24+ * However we may have full url redirect, on the same origin, we need to properly compare the origin then.
25+ * @param params
26+ */
27+ async function followSameOriginRedirect ( params : {
28+ url : string ,
29+ method : string ,
30+ headers : Record < string , string > ,
31+ /**
32+ * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers.
33+ */
34+ fetch ?: typeof fetch ;
35+ } ) : Promise < Response > {
36+ const resp = await ( params . fetch ?? fetch ) ( params . url , {
37+ method : params . method ,
38+ headers : params . headers ,
39+ // prevent automatic redirect
40+ redirect : 'manual' ,
41+ } ) ;
42+
43+ const location : string | null = resp . headers . get ( 'Location' ) ;
44+ if ( ! location ) return resp ;
45+
46+ // new URL('http://foo/bar', 'http://example.com/hello').href == http://foo/bar
47+ // new URL('/bar', 'http://example.com/hello').href == http://example.com/bar
48+ const nURL = new URL ( location , params . url ) ;
49+ // ensure origin are matching
50+ if ( new URL ( params . url ) . origin !== nURL . origin )
51+ return resp ;
52+
53+ return followSameOriginRedirect ( {
54+ ...params ,
55+ url : nURL . href ,
56+ } ) ;
57+ }
58+
1559/**
1660 * @returns null when the file doesn't exist
1761 */
@@ -47,38 +91,42 @@ export async function fileDownloadInfo(
4791 } /${ encodeURIComponent ( params . revision ?? "main" ) } /${ params . path } ` +
4892 ( params . noContentDisposition ? "?noContentDisposition=1" : "" ) ;
4993
50- const resp = await ( params . fetch ?? fetch ) ( url , {
51- method : "GET" ,
94+ //
95+ const resp = await followSameOriginRedirect ( {
96+ url : url ,
97+ method : "HEAD" ,
5298 headers : {
5399 ...( params . credentials && {
54100 Authorization : `Bearer ${ accessToken } ` ,
101+ // prevent any compression => we want to know the real size of the file
102+ 'Accept-Encoding' : 'identity' ,
55103 } ) ,
56- Range : "bytes=0-0" ,
57104 } ,
58105 } ) ;
59106
60107 if ( resp . status === 404 && resp . headers . get ( "X-Error-Code" ) === "EntryNotFound" ) {
61108 return null ;
62109 }
63110
64- if ( ! resp . ok ) {
111+ // redirect to CDN is okay not an error
112+ if ( ! resp . ok && ! resp . headers . get ( 'Location' ) ) {
65113 throw await createApiError ( resp ) ;
66114 }
67115
68- const etag = resp . headers . get ( "ETag" ) ;
69-
116+ // We favor a custom header indicating the etag of the linked resource, and
117+ // we fallback to the regular etag header.
118+ const etag = resp . headers . get ( HUGGINGFACE_HEADER_X_LINKED_ETAG ) ?? resp . headers . get ( "ETag" ) ;
70119 if ( ! etag ) {
71120 throw new InvalidApiResponseFormatError ( "Expected ETag" ) ;
72121 }
73122
74- const contentRangeHeader = resp . headers . get ( "content-range" ) ;
75-
76- if ( ! contentRangeHeader ) {
123+ // size is required
124+ const contentSize = resp . headers . get ( HUGGINGFACE_HEADER_X_LINKED_SIZE ) ?? resp . headers . get ( "Content-Length" )
125+ if ( ! contentSize ) {
77126 throw new InvalidApiResponseFormatError ( "Expected size information" ) ;
78127 }
79128
80- const [ , parsedSize ] = contentRangeHeader . split ( "/" ) ;
81- const size = parseInt ( parsedSize ) ;
129+ const size = parseInt ( contentSize ) ;
82130
83131 if ( isNaN ( size ) ) {
84132 throw new InvalidApiResponseFormatError ( "Invalid file size received" ) ;
@@ -87,6 +135,8 @@ export async function fileDownloadInfo(
87135 return {
88136 etag,
89137 size,
90- downloadLink : new URL ( resp . url ) . hostname !== new URL ( hubUrl ) . hostname ? resp . url : null ,
138+ // Either from response headers (if redirected) or defaults to request url
139+ downloadLink : resp . headers . get ( 'Location' ) ?? new URL ( resp . url ) . hostname !== new URL ( hubUrl ) . hostname ? resp . url : null ,
140+ commitHash : resp . headers . get ( HUGGINGFACE_HEADER_X_REPO_COMMIT ) ,
91141 } ;
92142}
0 commit comments