1
1
import parseLink from 'parse-link-header' ;
2
2
3
- import { registryAPI , repositoriesPerPage , tagsPerPage , usePortusExplore } from '@/options' ;
3
+ import { registryAPI , deleteEnabled , repositoriesPerPage , tagsPerPage , usePortusExplore } from '@/options' ;
4
4
5
5
function parseWWWAuthenticate ( text ) {
6
6
const result = { } ;
@@ -86,29 +86,41 @@ async function paginatable(path, scope, n, last = null) {
86
86
return Object . assign ( await response . json ( ) , { nextLast } ) ;
87
87
}
88
88
89
- async function get ( path , scope ) {
89
+ async function request ( method , path , scope , accept ) {
90
90
const url = new URL ( `${ await registryAPI ( ) } ${ path } ` ) ;
91
91
const headers = { } ;
92
- if ( scope ) {
93
- const token = await doAuth ( scope ) ;
94
- if ( token ) headers . Authorization = `Bearer ${ token } ` ;
92
+ if ( accept ) {
93
+ headers . Accept = accept ;
95
94
}
96
- const response = await fetch ( url , { headers } ) ;
97
- return response . json ( ) ;
98
- }
99
-
100
- async function head ( path , scope ) {
101
- const url = new URL ( `${ await registryAPI ( ) } ${ path } ` ) ;
102
- const headers = { } ;
103
95
if ( scope ) {
104
96
const token = await doAuth ( scope ) ;
105
97
if ( token ) headers . Authorization = `Bearer ${ token } ` ;
106
98
}
107
- const response = await fetch ( url , { method : 'HEAD' , headers } ) ;
108
- return response . headers ;
99
+ const response = await fetch ( url , { method, headers } ) ;
100
+ if ( ! response . ok ) {
101
+ if ( method === 'HEAD' ) {
102
+ throw new Error ( `${ response . statusText } ` ) ;
103
+ } else if ( response . headers . get ( 'Content-Type' ) . startsWith ( 'application/json' ) ) {
104
+ const r = await response . json ( ) ;
105
+ const firstError = r . errors [ 0 ] ;
106
+ if ( firstError ) {
107
+ throw new Error ( `${ firstError . code } : ${ firstError . message } ` ) ;
108
+ }
109
+ }
110
+ throw new Error ( `${ response . statusText } ` ) ;
111
+ }
112
+ if ( ! response . headers . get ( 'Content-Type' ) . startsWith ( 'application/json' ) ) {
113
+ console . warn ( 'response returned was not JSON, parsing may fail' ) ;
114
+ }
115
+ if ( method === 'HEAD' || parseInt ( response . headers . get ( 'Content-Length' ) , 10 ) < 1 ) {
116
+ return { headers : response . headers } ;
117
+ }
118
+ return {
119
+ ...( await response . json ( ) ) ,
120
+ headers : response . headers ,
121
+ } ;
109
122
}
110
123
111
-
112
124
async function portus ( ) {
113
125
// TODO: Use the Portus API when it enables anonymous access
114
126
const response = await fetch ( `${ await registryAPI ( ) } /explore?explore%5Bsearch%5D=` ) ;
@@ -135,10 +147,6 @@ async function repos(last = null) {
135
147
return paginatable ( '/v2/_catalog' , null , await repositoriesPerPage ( ) , last ) ;
136
148
}
137
149
138
- async function repo ( name ) {
139
- return get ( `/v2/${ name } ` , `repository:${ name } :pull` ) ;
140
- }
141
-
142
150
async function tags ( name , last = null ) {
143
151
if ( await usePortusExplore ( ) ) {
144
152
const p = await portus ( ) ;
@@ -151,20 +159,66 @@ async function tags(name, last = null) {
151
159
}
152
160
153
161
async function tag ( name , ref ) {
154
- return get ( `/v2/${ name } /manifests/${ ref } ` , `repository:${ name } :pull` ) ;
162
+ return request ( 'GET' , `/v2/${ name } /manifests/${ ref } ` , `repository:${ name } :pull` , 'application/vnd.docker.distribution.manifest.v2+json' ) ;
163
+ }
164
+
165
+ async function tagCanDelete ( name , ref ) {
166
+ if ( ! await deleteEnabled ( ) ) {
167
+ return false ;
168
+ }
169
+ try {
170
+ const { headers } = await request ( 'HEAD' , `/v2/${ name } /manifests/${ ref } ` , `repository:${ name } :delete` , 'application/vnd.docker.distribution.manifest.v2+json' ) ;
171
+ request ( 'HEAD' , `/v2/${ name } /manifests/${ headers . get ( 'Docker-Content-Digest' ) } ` , `repository:${ name } :delete` ) ;
172
+ return true ;
173
+ } catch ( e ) {
174
+ return false ;
175
+ }
176
+ }
177
+
178
+ async function tagDelete ( name , ref ) {
179
+ const tagManifest = await tag ( name , ref ) ;
180
+ // delete each blob
181
+ // await Promise.all(tagManifest.layers.map(l =>
182
+ // request('DELETE', `/v2/${name}/blobs/${l.digest}`, `repository:${name}:delete`)));
183
+ return request ( 'DELETE' , `/v2/${ name } /manifests/${ tagManifest . headers . get ( 'Docker-Content-Digest' ) } ` , `repository:${ name } :delete` ) ;
184
+ }
185
+
186
+ async function repoCanDelete ( name ) {
187
+ if ( ! await deleteEnabled ( ) ) {
188
+ return false ;
189
+ }
190
+ const r = await request ( 'GET' , `/v2/${ name } /tags/list` , `repository:${ name } :delete` ) ;
191
+ if ( ! r . tags ) {
192
+ return false ;
193
+ }
194
+ return Promise . race ( r . tags . map ( t => tagCanDelete ( name , t ) ) ) ;
195
+ }
196
+
197
+ async function repoDelete ( name ) {
198
+ const r = await request ( 'GET' , `/v2/${ name } /tags/list` , `repository:${ name } :delete` ) ;
199
+ return Promise . all ( r . tags . map ( t => tagDelete ( name , t ) ) ) ;
155
200
}
156
201
157
202
async function blob ( name , digest ) {
158
- const headers = await head ( `/v2/${ name } /blobs/${ digest } ` , `repository:${ name } :pull` ) ;
203
+ const { headers } = await request ( 'HEAD' , `/v2/${ name } /blobs/${ digest } ` , `repository:${ name } :pull` ) ;
159
204
return {
205
+ dockerContentDigest : headers . get ( 'Docker-Content-Digest' ) ,
160
206
contentLength : parseInt ( headers . get ( 'Content-Length' ) , 10 ) ,
161
207
} ;
162
208
}
163
209
210
+ async function configBlob ( name , digest ) {
211
+ return request ( 'GET' , `/v2/${ name } /blobs/${ digest } ` , `repository:${ name } :pull` ) ;
212
+ }
213
+
164
214
export {
165
215
repos ,
166
- repo ,
167
216
tags ,
168
217
tag ,
218
+ tagCanDelete ,
219
+ tagDelete ,
220
+ repoCanDelete ,
221
+ repoDelete ,
169
222
blob ,
223
+ configBlob ,
170
224
} ;
0 commit comments