gaoding-cli 1.0.0-alpha.21 → 1.0.0-alpha.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contracts/operations/dam.copy/input.schema.json +19 -0
- package/contracts/operations/dam.copy/output.schema.json +19 -0
- package/contracts/operations/dam.download/input.schema.json +14 -0
- package/contracts/operations/dam.download/output.schema.json +25 -0
- package/contracts/operations/dam.folder.list/input.schema.json +15 -0
- package/contracts/operations/dam.folder.list/output.schema.json +37 -0
- package/contracts/operations/dam.list/output.schema.json +3 -3
- package/contracts/operations/dam.move/input.schema.json +19 -0
- package/contracts/operations/dam.move/output.schema.json +18 -0
- package/contracts/operations/dam.recycle.list/input.schema.json +13 -0
- package/contracts/operations/dam.recycle.list/output.schema.json +34 -0
- package/contracts/operations/dam.recycle.restore/input.schema.json +14 -0
- package/contracts/operations/dam.recycle.restore/output.schema.json +6 -0
- package/contracts/operations/dam.rename/input.schema.json +18 -0
- package/contracts/operations/dam.rename/output.schema.json +18 -0
- package/contracts/operations/dam.repository.list/input.schema.json +8 -0
- package/contracts/operations/dam.repository.list/output.schema.json +52 -0
- package/contracts/operations/dam.search/output.schema.json +3 -3
- package/contracts/operations/dam.tag.list/input.schema.json +12 -0
- package/contracts/operations/dam.tag.list/output.schema.json +26 -0
- package/dist/src/bootstrap/create-runtime.js +24 -0
- package/dist/src/bootstrap/validators.js +39 -0
- package/dist/src/cli/dam-commands.js +248 -1
- package/dist/src/cli/errors.js +22 -2
- package/dist/src/cli/presenter.js +12 -0
- package/dist/src/features/dam/dam-api-adapter.js +369 -28
- package/dist/src/features/dam/file-downloader.js +242 -0
- package/dist/src/features/dam/operation-executor.js +72 -0
- package/dist/src/features/dam/repository-catalog.js +127 -0
- package/dist/src/features/dam/use-cases.js +323 -11
- package/dist/src/features/editor/bridge-server.js +6 -1
- package/dist/src/platform/signature.js +2 -1
- package/dist/src/platform/signed-http-transport.js +9 -1
- package/package.json +1 -1
- package/skills/gd-cli/SKILL.md +1 -1
- package/skills/gd-cli/references/dam.md +45 -13
- package/skills/gd-cli/references/editor.md +3 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { RemoteRequestError } from "../../platform/signed-http-transport.js";
|
|
2
|
+
import { RemoteProtocolError } from "../../platform/remote-protocol.js";
|
|
3
|
+
import { parseSafeRemoteAssetUrl, UrlSafetyError } from "../../platform/url-safety.js";
|
|
2
4
|
import { projectDamAssetDetail, projectDamAssetSummary } from "./asset-projection.js";
|
|
3
5
|
export class DamAssetNotFoundError extends Error {
|
|
4
6
|
constructor() {
|
|
@@ -6,6 +8,18 @@ export class DamAssetNotFoundError extends Error {
|
|
|
6
8
|
this.name = "DamAssetNotFoundError";
|
|
7
9
|
}
|
|
8
10
|
}
|
|
11
|
+
export class DamAssetNotDownloadableError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("该 DAM 素材当前不可下载。");
|
|
14
|
+
this.name = "DamAssetNotDownloadableError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class DamFolderNotFoundError extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("DAM 文件夹不存在。");
|
|
20
|
+
this.name = "DamFolderNotFoundError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
9
23
|
const contentFormats = {
|
|
10
24
|
image: ["gdpic", "gdimage"],
|
|
11
25
|
video: ["gdvideo"],
|
|
@@ -25,28 +39,245 @@ function nonblank(value) {
|
|
|
25
39
|
function finiteNumber(value) {
|
|
26
40
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
27
41
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
function nonnegativeInteger(value) {
|
|
43
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
44
|
+
}
|
|
45
|
+
const permissions = new Set(["OWNER", "MANAGE", "EDIT", "USE", "READ"]);
|
|
46
|
+
function folderPermission(value) {
|
|
47
|
+
return typeof value === "string"
|
|
48
|
+
&& permissions.has(value)
|
|
49
|
+
? value
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
function optionalString(value) {
|
|
53
|
+
if (value === undefined || value === null)
|
|
54
|
+
return undefined;
|
|
55
|
+
const projected = String(value);
|
|
56
|
+
return projected.trim() === "" ? undefined : projected;
|
|
57
|
+
}
|
|
58
|
+
function folderRows(value) {
|
|
59
|
+
if (!Array.isArray(value))
|
|
60
|
+
throw new RemoteProtocolError();
|
|
61
|
+
return value.flatMap((candidate) => {
|
|
62
|
+
const item = record(candidate);
|
|
63
|
+
const folderId = nonblank(item?.id);
|
|
64
|
+
const title = nonblank(item?.title);
|
|
65
|
+
const parentId = nonblank(item?.parent_id);
|
|
66
|
+
const repositoryId = nonblank(item?.repository_id);
|
|
67
|
+
if (folderId === undefined
|
|
68
|
+
|| title === undefined
|
|
69
|
+
|| parentId === undefined
|
|
70
|
+
|| repositoryId === undefined)
|
|
40
71
|
return [];
|
|
41
|
-
const
|
|
42
|
-
const
|
|
72
|
+
const permissionCode = folderPermission(item?.permission_code);
|
|
73
|
+
const path = nonblank(item?.path);
|
|
74
|
+
const createdAt = optionalString(item?.created_at);
|
|
75
|
+
const updatedAt = optionalString(item?.updated_at);
|
|
43
76
|
return [{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
folder_id: folderId,
|
|
78
|
+
title,
|
|
79
|
+
parent_id: parentId,
|
|
80
|
+
repository_id: repositoryId,
|
|
81
|
+
...(permissionCode === undefined ? {} : { permission_code: permissionCode }),
|
|
82
|
+
child_folder_count: nonnegativeInteger(item?.child_folder_count),
|
|
83
|
+
asset_count: nonnegativeInteger(item?.content_count),
|
|
84
|
+
...(path === undefined ? {} : { path }),
|
|
85
|
+
...(createdAt === undefined ? {} : { created_at: createdAt }),
|
|
86
|
+
...(updatedAt === undefined ? {} : { updated_at: updatedAt })
|
|
47
87
|
}];
|
|
48
88
|
});
|
|
49
89
|
}
|
|
90
|
+
function folderPagination(headers) {
|
|
91
|
+
const raw = headers.get("x-pagination");
|
|
92
|
+
if (raw === null)
|
|
93
|
+
throw new RemoteProtocolError();
|
|
94
|
+
let value;
|
|
95
|
+
try {
|
|
96
|
+
value = JSON.parse(raw);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new RemoteProtocolError();
|
|
100
|
+
}
|
|
101
|
+
const pagination = record(value);
|
|
102
|
+
const page = pagination?.num;
|
|
103
|
+
const pageSize = pagination?.size;
|
|
104
|
+
const total = pagination?.total;
|
|
105
|
+
if (!Number.isInteger(page) || page < 1
|
|
106
|
+
|| !Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100
|
|
107
|
+
|| !Number.isInteger(total) || total < 0) {
|
|
108
|
+
throw new RemoteProtocolError();
|
|
109
|
+
}
|
|
110
|
+
return { page: page, pageSize: pageSize, total: total };
|
|
111
|
+
}
|
|
112
|
+
function tagRows(value) {
|
|
113
|
+
if (!Array.isArray(value))
|
|
114
|
+
throw new RemoteProtocolError();
|
|
115
|
+
return value.flatMap((candidate) => {
|
|
116
|
+
const item = record(candidate);
|
|
117
|
+
const tagId = nonblank(item?.id);
|
|
118
|
+
const name = nonblank(item?.name);
|
|
119
|
+
if (tagId === undefined || name === undefined)
|
|
120
|
+
return [];
|
|
121
|
+
const groupId = nonblank(item?.group_id);
|
|
122
|
+
return [{
|
|
123
|
+
tag_id: tagId,
|
|
124
|
+
name,
|
|
125
|
+
...(groupId === undefined ? {} : { group_id: groupId }),
|
|
126
|
+
asset_count: nonnegativeInteger(item?.file_count)
|
|
127
|
+
}];
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function downloadUrls(value) {
|
|
131
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
132
|
+
throw new RemoteProtocolError();
|
|
133
|
+
}
|
|
134
|
+
const urls = value.filter((item) => (typeof item === "string" && item.trim() !== ""));
|
|
135
|
+
if (urls.length === 0)
|
|
136
|
+
throw new DamAssetNotDownloadableError();
|
|
137
|
+
for (const value of urls) {
|
|
138
|
+
try {
|
|
139
|
+
const url = parseSafeRemoteAssetUrl(value);
|
|
140
|
+
if (url.protocol !== "https:")
|
|
141
|
+
throw new UrlSafetyError();
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof UrlSafetyError)
|
|
145
|
+
throw new RemoteProtocolError();
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return urls;
|
|
150
|
+
}
|
|
151
|
+
function folderDetail(response) {
|
|
152
|
+
for (const source of responseRecords(response).reverse()) {
|
|
153
|
+
const entryId = nonblank(source.id);
|
|
154
|
+
const title = nonblank(source.title);
|
|
155
|
+
const repositoryId = nonblank(source.repository_id);
|
|
156
|
+
const parentId = nonblank(source.parent_id);
|
|
157
|
+
if (entryId !== undefined
|
|
158
|
+
&& title !== undefined
|
|
159
|
+
&& repositoryId !== undefined
|
|
160
|
+
&& parentId !== undefined) {
|
|
161
|
+
return {
|
|
162
|
+
entry_id: entryId,
|
|
163
|
+
kind: "folder",
|
|
164
|
+
title,
|
|
165
|
+
repository_id: repositoryId,
|
|
166
|
+
folder_id: parentId
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new RemoteProtocolError();
|
|
171
|
+
}
|
|
172
|
+
function operationSubmission(response) {
|
|
173
|
+
const root = record(response);
|
|
174
|
+
const source = record(root?.data) ?? root;
|
|
175
|
+
if (source === undefined)
|
|
176
|
+
throw new RemoteProtocolError();
|
|
177
|
+
const rawTaskId = source.task_id;
|
|
178
|
+
let taskId;
|
|
179
|
+
if (rawTaskId !== undefined && rawTaskId !== null) {
|
|
180
|
+
if (typeof rawTaskId === "number") {
|
|
181
|
+
if (!Number.isSafeInteger(rawTaskId) || rawTaskId < 0)
|
|
182
|
+
throw new RemoteProtocolError();
|
|
183
|
+
if (rawTaskId !== 0)
|
|
184
|
+
taskId = String(rawTaskId);
|
|
185
|
+
}
|
|
186
|
+
else if (typeof rawTaskId === "string" && rawTaskId.trim() !== "") {
|
|
187
|
+
if (rawTaskId.trim() !== "0")
|
|
188
|
+
taskId = rawTaskId.trim();
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
throw new RemoteProtocolError();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const rawResultIds = source.result_ids;
|
|
195
|
+
if (rawResultIds !== undefined && !Array.isArray(rawResultIds)) {
|
|
196
|
+
throw new RemoteProtocolError();
|
|
197
|
+
}
|
|
198
|
+
const resultIds = Array.isArray(rawResultIds)
|
|
199
|
+
? rawResultIds.filter((item) => nonblank(item) !== undefined)
|
|
200
|
+
: [];
|
|
201
|
+
return {
|
|
202
|
+
...(taskId === undefined ? {} : { task_id: taskId }),
|
|
203
|
+
...(rawResultIds === undefined ? {} : { result_ids: resultIds })
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function taskRows(response) {
|
|
207
|
+
if (Array.isArray(response))
|
|
208
|
+
return response;
|
|
209
|
+
const data = record(response)?.data;
|
|
210
|
+
if (Array.isArray(data))
|
|
211
|
+
return data;
|
|
212
|
+
throw new RemoteProtocolError();
|
|
213
|
+
}
|
|
214
|
+
function operationTask(response, taskId) {
|
|
215
|
+
const matches = taskRows(response).flatMap((candidate) => {
|
|
216
|
+
const item = record(candidate);
|
|
217
|
+
return nonblank(item?.id) === taskId ? [item] : [];
|
|
218
|
+
});
|
|
219
|
+
if (matches.length === 0)
|
|
220
|
+
return null;
|
|
221
|
+
if (matches.length !== 1)
|
|
222
|
+
throw new RemoteProtocolError();
|
|
223
|
+
const item = matches[0];
|
|
224
|
+
const status = item.status;
|
|
225
|
+
const progress = item.progress;
|
|
226
|
+
const totalCount = item.total_count;
|
|
227
|
+
const successCount = item.success_count;
|
|
228
|
+
const failCount = item.fail_count;
|
|
229
|
+
if (!Number.isInteger(status)
|
|
230
|
+
|| typeof progress !== "number" || !Number.isFinite(progress) || progress < 0
|
|
231
|
+
|| (totalCount !== null
|
|
232
|
+
&& (!Number.isInteger(totalCount) || totalCount < 0))
|
|
233
|
+
|| (successCount !== null
|
|
234
|
+
&& (!Number.isInteger(successCount) || successCount < 0))
|
|
235
|
+
|| (failCount !== null
|
|
236
|
+
&& (!Number.isInteger(failCount) || failCount < 0))) {
|
|
237
|
+
throw new RemoteProtocolError();
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
id: taskId,
|
|
241
|
+
status: status,
|
|
242
|
+
progress,
|
|
243
|
+
total_count: totalCount,
|
|
244
|
+
success_count: successCount,
|
|
245
|
+
fail_count: failCount
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function operationChildren(response) {
|
|
249
|
+
const root = record(response);
|
|
250
|
+
const source = record(root?.data) ?? root;
|
|
251
|
+
if (source === undefined)
|
|
252
|
+
throw new RemoteProtocolError();
|
|
253
|
+
const rawResultIds = source.result_ids;
|
|
254
|
+
if (rawResultIds !== undefined && !Array.isArray(rawResultIds)) {
|
|
255
|
+
throw new RemoteProtocolError();
|
|
256
|
+
}
|
|
257
|
+
const repositoryId = nonblank(source.repository_id);
|
|
258
|
+
const targetRepositoryId = nonblank(source.target_repository_id);
|
|
259
|
+
const targetParentId = nonblank(source.target_parent_id);
|
|
260
|
+
return {
|
|
261
|
+
result_ids: Array.isArray(rawResultIds)
|
|
262
|
+
? rawResultIds.filter((item) => nonblank(item) !== undefined)
|
|
263
|
+
: [],
|
|
264
|
+
...(repositoryId === undefined ? {} : { repository_id: repositoryId }),
|
|
265
|
+
...(targetRepositoryId === undefined ? {} : {
|
|
266
|
+
target_repository_id: targetRepositoryId
|
|
267
|
+
}),
|
|
268
|
+
...(targetParentId === undefined ? {} : { target_parent_id: targetParentId })
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function assertSingleOperationInput(input) {
|
|
272
|
+
if (nonblank(input.entryId) === undefined
|
|
273
|
+
|| (input.kind !== "asset" && input.kind !== "folder")
|
|
274
|
+
|| (input.operation !== "move" && input.operation !== "copy")
|
|
275
|
+
|| nonblank(input.sourceRepositoryId) === undefined
|
|
276
|
+
|| nonblank(input.targetRepositoryId) === undefined
|
|
277
|
+
|| nonblank(input.targetFolderId) === undefined) {
|
|
278
|
+
throw new TypeError("Invalid DAM single-entry operation input.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
50
281
|
function assetId(response) {
|
|
51
282
|
const root = record(response);
|
|
52
283
|
const nested = record(root?.data);
|
|
@@ -120,13 +351,6 @@ function recycleBody(input) {
|
|
|
120
351
|
}
|
|
121
352
|
export function createDamApiAdapter(dependencies) {
|
|
122
353
|
return {
|
|
123
|
-
async listRepositories(input) {
|
|
124
|
-
const response = await dependencies.transport.getJson({
|
|
125
|
-
path: "/tb-dam/repositories",
|
|
126
|
-
...requestContext(input)
|
|
127
|
-
});
|
|
128
|
-
return repositories(response);
|
|
129
|
-
},
|
|
130
354
|
async search(input) {
|
|
131
355
|
const response = await dependencies.transport.postJson({
|
|
132
356
|
path: "/dam/asset/v2/search/simple",
|
|
@@ -137,18 +361,16 @@ export function createDamApiAdapter(dependencies) {
|
|
|
137
361
|
include_child_folder: input.includeChildFolder,
|
|
138
362
|
recycle: false,
|
|
139
363
|
page_size: input.pageSize,
|
|
140
|
-
page_num: 1,
|
|
141
|
-
page: 1,
|
|
142
364
|
sort: ["-top_time", "-updated_at"],
|
|
143
365
|
...(input.cursor === undefined
|
|
144
366
|
? {}
|
|
145
|
-
: { query_id: input.cursor
|
|
367
|
+
: { query_id: input.cursor }),
|
|
146
368
|
...(input.type === undefined
|
|
147
369
|
? {}
|
|
148
370
|
: { content_format_list: contentFormats[input.type] }),
|
|
149
371
|
...(input.assetFormats === undefined || input.assetFormats.length === 0
|
|
150
372
|
? {}
|
|
151
|
-
: {
|
|
373
|
+
: { format: input.assetFormats }),
|
|
152
374
|
...(input.tagIds === undefined || input.tagIds.length === 0
|
|
153
375
|
? {}
|
|
154
376
|
: { tag_ids: input.tagIds })
|
|
@@ -157,6 +379,125 @@ export function createDamApiAdapter(dependencies) {
|
|
|
157
379
|
});
|
|
158
380
|
return searchResult(response, input);
|
|
159
381
|
},
|
|
382
|
+
async listFolders(input) {
|
|
383
|
+
const response = await dependencies.transport.getJsonResponse({
|
|
384
|
+
path: "/tb-dam/folders",
|
|
385
|
+
query: {
|
|
386
|
+
repository_ids: input.repositoryId,
|
|
387
|
+
parent_id: input.parentId,
|
|
388
|
+
keyword: input.query ?? "",
|
|
389
|
+
page_num: String(input.page),
|
|
390
|
+
page_size: String(input.pageSize),
|
|
391
|
+
filter_no_permission: "true",
|
|
392
|
+
include_children: String(input.query !== undefined),
|
|
393
|
+
sort: "topTime:desc,updatedAt:desc"
|
|
394
|
+
},
|
|
395
|
+
...requestContext(input)
|
|
396
|
+
});
|
|
397
|
+
const pagination = folderPagination(response.headers);
|
|
398
|
+
return {
|
|
399
|
+
repository_id: input.repositoryId,
|
|
400
|
+
parent_id: input.parentId,
|
|
401
|
+
page: pagination.page,
|
|
402
|
+
page_size: pagination.pageSize,
|
|
403
|
+
total: pagination.total,
|
|
404
|
+
folders: folderRows(response.body)
|
|
405
|
+
};
|
|
406
|
+
},
|
|
407
|
+
async listTags(input) {
|
|
408
|
+
const response = await dependencies.transport.getJson({
|
|
409
|
+
path: "/tb-dam/tags",
|
|
410
|
+
query: {
|
|
411
|
+
repository_id: input.repositoryId,
|
|
412
|
+
group_id: "",
|
|
413
|
+
...(input.query === undefined ? {} : { keyword: input.query })
|
|
414
|
+
},
|
|
415
|
+
...requestContext(input)
|
|
416
|
+
});
|
|
417
|
+
return { repository_id: input.repositoryId, tags: tagRows(response) };
|
|
418
|
+
},
|
|
419
|
+
async getDownloadUrls(input) {
|
|
420
|
+
const response = await dependencies.transport.getJson({
|
|
421
|
+
path: `/dam/asset/files/download/${encodeURIComponent(input.assetId)}/url-list`,
|
|
422
|
+
query: { repository_id: input.repositoryId },
|
|
423
|
+
...requestContext(input)
|
|
424
|
+
});
|
|
425
|
+
return downloadUrls(response);
|
|
426
|
+
},
|
|
427
|
+
async getFolder(input) {
|
|
428
|
+
let response;
|
|
429
|
+
try {
|
|
430
|
+
response = await dependencies.transport.getJson({
|
|
431
|
+
path: `/dam/folders/${encodeURIComponent(input.folderId)}`,
|
|
432
|
+
query: { repository_id: input.repositoryId },
|
|
433
|
+
...requestContext(input)
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
if (error instanceof RemoteRequestError && error.status === 404) {
|
|
438
|
+
throw new DamFolderNotFoundError();
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
return folderDetail(response);
|
|
443
|
+
},
|
|
444
|
+
async renameEntry(input) {
|
|
445
|
+
await dependencies.transport.putJson({
|
|
446
|
+
path: input.kind === "asset"
|
|
447
|
+
? `/dam/asset/${encodeURIComponent(input.entryId)}/rename`
|
|
448
|
+
: `/dam/folders/${encodeURIComponent(input.entryId)}/rename`,
|
|
449
|
+
body: {
|
|
450
|
+
id: input.entryId,
|
|
451
|
+
repository_id: input.repositoryId,
|
|
452
|
+
title: input.title
|
|
453
|
+
},
|
|
454
|
+
...requestContext(input)
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
async submitEntryOperation(input) {
|
|
458
|
+
assertSingleOperationInput(input);
|
|
459
|
+
const asset = input.kind === "asset";
|
|
460
|
+
const body = asset
|
|
461
|
+
? {
|
|
462
|
+
source_infos: [{
|
|
463
|
+
source_repository_id: input.sourceRepositoryId,
|
|
464
|
+
content_ids: [input.entryId]
|
|
465
|
+
}],
|
|
466
|
+
target_folder_id: input.targetFolderId,
|
|
467
|
+
target_repository_id: input.targetRepositoryId,
|
|
468
|
+
...(input.operation === "copy" ? { expand: false } : {})
|
|
469
|
+
}
|
|
470
|
+
: {
|
|
471
|
+
folder_ids: [input.entryId],
|
|
472
|
+
parent_id: input.targetFolderId,
|
|
473
|
+
repository_id: input.sourceRepositoryId,
|
|
474
|
+
target_repository_id: input.targetRepositoryId,
|
|
475
|
+
...(input.operation === "copy" ? { expand: false } : {})
|
|
476
|
+
};
|
|
477
|
+
const response = await dependencies.transport.postJson({
|
|
478
|
+
path: asset
|
|
479
|
+
? `/dam/asset/multi-repository/${input.operation}`
|
|
480
|
+
: `/dam/folders/v2/${input.operation}`,
|
|
481
|
+
body,
|
|
482
|
+
...requestContext(input)
|
|
483
|
+
});
|
|
484
|
+
return operationSubmission(response);
|
|
485
|
+
},
|
|
486
|
+
async getOperationTask(input) {
|
|
487
|
+
const response = await dependencies.transport.getJson({
|
|
488
|
+
path: "/dam/task/list",
|
|
489
|
+
query: { ids: input.taskId },
|
|
490
|
+
...requestContext(input)
|
|
491
|
+
});
|
|
492
|
+
return operationTask(response, input.taskId);
|
|
493
|
+
},
|
|
494
|
+
async getOperationChildren(input) {
|
|
495
|
+
const response = await dependencies.transport.getJson({
|
|
496
|
+
path: `/dam/task/children-detail/${encodeURIComponent(input.taskId)}`,
|
|
497
|
+
...requestContext(input)
|
|
498
|
+
});
|
|
499
|
+
return operationChildren(response);
|
|
500
|
+
},
|
|
160
501
|
async getAsset(input) {
|
|
161
502
|
let response;
|
|
162
503
|
try {
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { access, mkdir, open, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { extname, resolve } from "node:path";
|
|
4
|
+
import { parseSafeRemoteAssetUrl } from "../../platform/url-safety.js";
|
|
5
|
+
export class DamDownloadError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("素材下载失败。");
|
|
8
|
+
this.name = "DamDownloadError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class DamDownloadDestinationExistsError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("目标文件已存在,未覆盖任何文件。");
|
|
14
|
+
this.name = "DamDownloadDestinationExistsError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const redirectStatuses = new Set([301, 302, 303, 307, 308]);
|
|
18
|
+
const maximumRedirects = 5;
|
|
19
|
+
const maximumFilenameLength = 180;
|
|
20
|
+
const nodeFileSystem = {
|
|
21
|
+
async mkdir(path, options) {
|
|
22
|
+
await mkdir(path, options);
|
|
23
|
+
},
|
|
24
|
+
async open(path, flags, mode) {
|
|
25
|
+
const handle = await open(path, flags, mode);
|
|
26
|
+
return {
|
|
27
|
+
write: async (data) => handle.write(data),
|
|
28
|
+
sync: async () => handle.sync(),
|
|
29
|
+
close: async () => handle.close()
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
access,
|
|
33
|
+
rename,
|
|
34
|
+
async rm(path, options) {
|
|
35
|
+
await rm(path, options);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function safeDownloadUrl(value) {
|
|
39
|
+
try {
|
|
40
|
+
const url = parseSafeRemoteAssetUrl(value);
|
|
41
|
+
if (url.protocol !== "https:")
|
|
42
|
+
throw new Error("HTTPS required");
|
|
43
|
+
return url;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new DamDownloadError();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function decodedFilename(value) {
|
|
50
|
+
const encoded = /(?:^|;)\s*filename\*\s*=\s*(?:UTF-8'')?("?)([^;"]*)\1/iu.exec(value);
|
|
51
|
+
if (encoded?.[2] !== undefined) {
|
|
52
|
+
try {
|
|
53
|
+
return decodeURIComponent(encoded[2].trim());
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const plain = /(?:^|;)\s*filename\s*=\s*(?:"([^"]*)"|([^;]*))/iu.exec(value);
|
|
60
|
+
return plain?.[1]?.trim() ?? plain?.[2]?.trim();
|
|
61
|
+
}
|
|
62
|
+
function safeFormat(value) {
|
|
63
|
+
const format = value?.trim().replace(/^\.+/u, "").replace(/[^a-zA-Z0-9]+/gu, "");
|
|
64
|
+
return format === undefined || format === "" ? "bin" : format.toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
function truncateFilename(value) {
|
|
67
|
+
if (value.length <= maximumFilenameLength)
|
|
68
|
+
return value;
|
|
69
|
+
const extension = extname(value);
|
|
70
|
+
const extensionLength = Math.min(extension.length, 20);
|
|
71
|
+
const safeExtension = extension.slice(0, extensionLength);
|
|
72
|
+
const stemLength = maximumFilenameLength - safeExtension.length;
|
|
73
|
+
return `${value.slice(0, Math.max(1, stemLength))}${safeExtension}`;
|
|
74
|
+
}
|
|
75
|
+
function safeFilename(candidate, fallback, format) {
|
|
76
|
+
const raw = (candidate === undefined || candidate.trim() === "") ? fallback : candidate;
|
|
77
|
+
const segments = raw.normalize("NFKC").replace(/[\u0000-\u001f\u007f]/gu, "")
|
|
78
|
+
.split(/[\\/]+/u);
|
|
79
|
+
let filename = (segments.at(-1) ?? "").trim().replace(/^\.+/u, "");
|
|
80
|
+
if (filename === "" || filename === "." || filename === "..")
|
|
81
|
+
filename = "asset";
|
|
82
|
+
if (extname(filename) === "")
|
|
83
|
+
filename = `${filename}.${format}`;
|
|
84
|
+
return truncateFilename(filename);
|
|
85
|
+
}
|
|
86
|
+
function withIndex(filename, index, total) {
|
|
87
|
+
if (total === 1)
|
|
88
|
+
return filename;
|
|
89
|
+
const extension = extname(filename);
|
|
90
|
+
const stem = extension === "" ? filename : filename.slice(0, -extension.length);
|
|
91
|
+
return truncateFilename(`${stem}-${index + 1}${extension}`);
|
|
92
|
+
}
|
|
93
|
+
async function fetchDownload(initial, fetch, signal) {
|
|
94
|
+
let url = safeDownloadUrl(initial);
|
|
95
|
+
for (let redirects = 0; redirects <= maximumRedirects; redirects += 1) {
|
|
96
|
+
signal.throwIfAborted();
|
|
97
|
+
let response;
|
|
98
|
+
try {
|
|
99
|
+
response = await fetch(url, { method: "GET", redirect: "manual", signal });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
if (signal.aborted)
|
|
103
|
+
throw signal.reason;
|
|
104
|
+
throw new DamDownloadError();
|
|
105
|
+
}
|
|
106
|
+
signal.throwIfAborted();
|
|
107
|
+
if (!redirectStatuses.has(response.status)) {
|
|
108
|
+
if (!response.ok || response.body === null)
|
|
109
|
+
throw new DamDownloadError();
|
|
110
|
+
return response;
|
|
111
|
+
}
|
|
112
|
+
const location = response.headers.get("location");
|
|
113
|
+
if (location === null || redirects === maximumRedirects)
|
|
114
|
+
throw new DamDownloadError();
|
|
115
|
+
try {
|
|
116
|
+
url = safeDownloadUrl(new URL(location, url).href);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new DamDownloadError();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await response.body?.cancel().catch(() => undefined);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new DamDownloadError();
|
|
126
|
+
}
|
|
127
|
+
async function writeAll(handle, data) {
|
|
128
|
+
let offset = 0;
|
|
129
|
+
while (offset < data.byteLength) {
|
|
130
|
+
const { bytesWritten } = await handle.write(data.subarray(offset));
|
|
131
|
+
if (!Number.isInteger(bytesWritten) || bytesWritten <= 0)
|
|
132
|
+
throw new DamDownloadError();
|
|
133
|
+
offset += bytesWritten;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function isMissingFile(error) {
|
|
137
|
+
return error !== null
|
|
138
|
+
&& typeof error === "object"
|
|
139
|
+
&& "code" in error
|
|
140
|
+
&& error.code === "ENOENT";
|
|
141
|
+
}
|
|
142
|
+
export function createDamFileDownloader(options = {}) {
|
|
143
|
+
const dependencies = {
|
|
144
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
145
|
+
fileSystem: options.fileSystem ?? nodeFileSystem,
|
|
146
|
+
randomId: options.randomId ?? randomUUID,
|
|
147
|
+
cwd: options.cwd ?? process.cwd
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
async download(input) {
|
|
151
|
+
input.signal.throwIfAborted();
|
|
152
|
+
if (input.urls.length === 0)
|
|
153
|
+
throw new DamDownloadError();
|
|
154
|
+
const outputDirectory = resolve(dependencies.cwd(), input.outputDirectory);
|
|
155
|
+
const pending = [];
|
|
156
|
+
const createdFinalPaths = [];
|
|
157
|
+
try {
|
|
158
|
+
await dependencies.fileSystem.mkdir(outputDirectory, { recursive: true });
|
|
159
|
+
for (const [index, rawUrl] of input.urls.entries()) {
|
|
160
|
+
input.signal.throwIfAborted();
|
|
161
|
+
const response = await fetchDownload(rawUrl, dependencies.fetch, input.signal);
|
|
162
|
+
const format = safeFormat(input.fallbackFormat);
|
|
163
|
+
const fallback = safeFilename(input.fallbackTitle, "asset", format);
|
|
164
|
+
const disposition = response.headers.get("content-disposition");
|
|
165
|
+
const filename = withIndex(safeFilename(disposition === null ? undefined : decodedFilename(disposition), fallback, format), index, input.urls.length);
|
|
166
|
+
const finalPath = resolve(outputDirectory, filename);
|
|
167
|
+
if (!finalPath.startsWith(`${outputDirectory}/`) && finalPath !== outputDirectory) {
|
|
168
|
+
throw new DamDownloadError();
|
|
169
|
+
}
|
|
170
|
+
const temporaryPath = resolve(outputDirectory, `.part-${dependencies.randomId()}-${index + 1}`);
|
|
171
|
+
const handle = await dependencies.fileSystem.open(temporaryPath, "wx", 0o600);
|
|
172
|
+
const item = {
|
|
173
|
+
temporaryPath,
|
|
174
|
+
finalPath,
|
|
175
|
+
handle,
|
|
176
|
+
open: true,
|
|
177
|
+
bytes: 0,
|
|
178
|
+
sha256: ""
|
|
179
|
+
};
|
|
180
|
+
pending.push(item);
|
|
181
|
+
const hash = createHash("sha256");
|
|
182
|
+
const reader = response.body.getReader();
|
|
183
|
+
try {
|
|
184
|
+
while (true) {
|
|
185
|
+
input.signal.throwIfAborted();
|
|
186
|
+
const chunk = await reader.read();
|
|
187
|
+
if (chunk.done)
|
|
188
|
+
break;
|
|
189
|
+
await writeAll(handle, chunk.value);
|
|
190
|
+
hash.update(chunk.value);
|
|
191
|
+
item.bytes += chunk.value.byteLength;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
reader.releaseLock();
|
|
196
|
+
}
|
|
197
|
+
item.sha256 = hash.digest("hex");
|
|
198
|
+
}
|
|
199
|
+
for (const item of pending) {
|
|
200
|
+
try {
|
|
201
|
+
await dependencies.fileSystem.access(item.finalPath);
|
|
202
|
+
throw new DamDownloadDestinationExistsError();
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof DamDownloadDestinationExistsError)
|
|
206
|
+
throw error;
|
|
207
|
+
if (!isMissingFile(error))
|
|
208
|
+
throw new DamDownloadError();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const item of pending) {
|
|
212
|
+
await item.handle.sync();
|
|
213
|
+
await item.handle.close();
|
|
214
|
+
item.open = false;
|
|
215
|
+
}
|
|
216
|
+
for (const item of pending) {
|
|
217
|
+
await dependencies.fileSystem.rename(item.temporaryPath, item.finalPath);
|
|
218
|
+
createdFinalPaths.push(item.finalPath);
|
|
219
|
+
}
|
|
220
|
+
return pending.map((item) => ({
|
|
221
|
+
path: item.finalPath,
|
|
222
|
+
bytes: item.bytes,
|
|
223
|
+
sha256: item.sha256
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
await Promise.all(pending.map(async (item) => {
|
|
228
|
+
if (item.open)
|
|
229
|
+
await item.handle.close().catch(() => undefined);
|
|
230
|
+
await dependencies.fileSystem.rm(item.temporaryPath, { force: true }).catch(() => undefined);
|
|
231
|
+
}));
|
|
232
|
+
await Promise.all(createdFinalPaths.map((path) => (dependencies.fileSystem.rm(path, { force: true }).catch(() => undefined))));
|
|
233
|
+
if (input.signal.aborted)
|
|
234
|
+
throw input.signal.reason;
|
|
235
|
+
if (error instanceof DamDownloadDestinationExistsError
|
|
236
|
+
|| error instanceof DamDownloadError)
|
|
237
|
+
throw error;
|
|
238
|
+
throw new DamDownloadError();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|