@@ -4,14 +4,58 @@ import type { CredentialsParams, RepoDesignation } from "../types/public";
4
4
import { checkCredentials } from "../utils/checkCredentials" ;
5
5
import { toRepoId } from "../utils/toRepoId" ;
6
6
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
+
7
11
export interface FileDownloadInfoOutput {
8
12
size : number ;
9
13
etag : string ;
14
+ commitHash : string | null ;
10
15
/**
11
16
* In case of LFS file, link to download directly from cloud provider
12
17
*/
13
18
downloadLink : string | null ;
14
19
}
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
+
15
59
/**
16
60
* @returns null when the file doesn't exist
17
61
*/
@@ -47,38 +91,42 @@ export async function fileDownloadInfo(
47
91
} /${ encodeURIComponent ( params . revision ?? "main" ) } /${ params . path } ` +
48
92
( params . noContentDisposition ? "?noContentDisposition=1" : "" ) ;
49
93
50
- const resp = await ( params . fetch ?? fetch ) ( url , {
51
- method : "GET" ,
94
+ //
95
+ const resp = await followSameOriginRedirect ( {
96
+ url : url ,
97
+ method : "HEAD" ,
52
98
headers : {
53
99
...( params . credentials && {
54
100
Authorization : `Bearer ${ accessToken } ` ,
101
+ // prevent any compression => we want to know the real size of the file
102
+ 'Accept-Encoding' : 'identity' ,
55
103
} ) ,
56
- Range : "bytes=0-0" ,
57
104
} ,
58
105
} ) ;
59
106
60
107
if ( resp . status === 404 && resp . headers . get ( "X-Error-Code" ) === "EntryNotFound" ) {
61
108
return null ;
62
109
}
63
110
64
- if ( ! resp . ok ) {
111
+ // redirect to CDN is okay not an error
112
+ if ( ! resp . ok && ! resp . headers . get ( 'Location' ) ) {
65
113
throw await createApiError ( resp ) ;
66
114
}
67
115
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" ) ;
70
119
if ( ! etag ) {
71
120
throw new InvalidApiResponseFormatError ( "Expected ETag" ) ;
72
121
}
73
122
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 ) {
77
126
throw new InvalidApiResponseFormatError ( "Expected size information" ) ;
78
127
}
79
128
80
- const [ , parsedSize ] = contentRangeHeader . split ( "/" ) ;
81
- const size = parseInt ( parsedSize ) ;
129
+ const size = parseInt ( contentSize ) ;
82
130
83
131
if ( isNaN ( size ) ) {
84
132
throw new InvalidApiResponseFormatError ( "Invalid file size received" ) ;
@@ -87,6 +135,8 @@ export async function fileDownloadInfo(
87
135
return {
88
136
etag,
89
137
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 ) ,
91
141
} ;
92
142
}
0 commit comments