@things-factory/attachment-base 10.0.0-beta.7 → 10.0.0-beta.71
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/dist-server/service/attachment/attachment-mutation.d.ts +2 -0
- package/dist-server/service/attachment/attachment-mutation.js +54 -13
- package/dist-server/service/attachment/attachment-mutation.js.map +1 -1
- package/dist-server/service/attachment/attachment.d.ts +7 -0
- package/dist-server/service/attachment/attachment.js +13 -1
- package/dist-server/service/attachment/attachment.js.map +1 -1
- package/dist-server/service/attachment/gltf-thumbnail.d.ts +6 -0
- package/dist-server/service/attachment/gltf-thumbnail.js +208 -0
- package/dist-server/service/attachment/gltf-thumbnail.js.map +1 -0
- package/dist-server/service/attachment/mimetype.d.ts +47 -0
- package/dist-server/service/attachment/mimetype.js +111 -0
- package/dist-server/service/attachment/mimetype.js.map +1 -0
- package/dist-server/service/attachment/thumbnail-backfill.d.ts +17 -0
- package/dist-server/service/attachment/thumbnail-backfill.js +115 -0
- package/dist-server/service/attachment/thumbnail-backfill.js.map +1 -0
- package/dist-server/service/attachment/thumbnail.d.ts +13 -0
- package/dist-server/service/attachment/thumbnail.js +47 -0
- package/dist-server/service/attachment/thumbnail.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FileUpload } from 'graphql-upload/GraphQLUpload.js';
|
|
2
2
|
import { Attachment } from './attachment';
|
|
3
3
|
import { AttachmentPatch, NewAttachment } from './attachment-types';
|
|
4
|
+
import { ThumbnailBackfillResult } from './thumbnail-backfill';
|
|
4
5
|
export declare class AttachmentMutation {
|
|
5
6
|
createAttachment(attachment: NewAttachment, context: ResolverContext): Promise<Attachment>;
|
|
6
7
|
createAttachments(attachments: NewAttachment[], context: ResolverContext): Promise<Attachment[]>;
|
|
@@ -16,6 +17,7 @@ export declare class AttachmentMutation {
|
|
|
16
17
|
};
|
|
17
18
|
}>;
|
|
18
19
|
importAttachments(file: FileUpload, context: ResolverContext): Promise<Attachment[]>;
|
|
20
|
+
backfillAttachmentThumbnails(limit: number, context: ResolverContext): Promise<ThumbnailBackfillResult>;
|
|
19
21
|
}
|
|
20
22
|
export declare function createAttachment(_: any, { attachment }: {
|
|
21
23
|
attachment: any;
|
|
@@ -21,6 +21,9 @@ const shell_1 = require("@things-factory/shell");
|
|
|
21
21
|
const attachment_const_1 = require("../../attachment-const");
|
|
22
22
|
const attachment_1 = require("./attachment");
|
|
23
23
|
const attachment_types_1 = require("./attachment-types");
|
|
24
|
+
const mimetype_1 = require("./mimetype");
|
|
25
|
+
const thumbnail_1 = require("./thumbnail");
|
|
26
|
+
const thumbnail_backfill_1 = require("./thumbnail-backfill");
|
|
24
27
|
const allowedMimeTypes = env_1.config.get('fileUpload/mimeTypes', []);
|
|
25
28
|
let AttachmentMutation = class AttachmentMutation {
|
|
26
29
|
async createAttachment(attachment, context) {
|
|
@@ -60,6 +63,9 @@ let AttachmentMutation = class AttachmentMutation {
|
|
|
60
63
|
async importAttachments(file, context) {
|
|
61
64
|
return await importAttachments(file, context);
|
|
62
65
|
}
|
|
66
|
+
async backfillAttachmentThumbnails(limit, context) {
|
|
67
|
+
return await (0, thumbnail_backfill_1.backfillAttachmentThumbnails)({ limit }, context);
|
|
68
|
+
}
|
|
63
69
|
};
|
|
64
70
|
exports.AttachmentMutation = AttachmentMutation;
|
|
65
71
|
tslib_1.__decorate([
|
|
@@ -153,24 +159,58 @@ tslib_1.__decorate([
|
|
|
153
159
|
tslib_1.__metadata("design:paramtypes", [typeof (_b = typeof GraphQLUpload_js_1.FileUpload !== "undefined" && GraphQLUpload_js_1.FileUpload) === "function" ? _b : Object, Object]),
|
|
154
160
|
tslib_1.__metadata("design:returntype", Promise)
|
|
155
161
|
], AttachmentMutation.prototype, "importAttachments", null);
|
|
162
|
+
tslib_1.__decorate([
|
|
163
|
+
(0, type_graphql_1.Directive)('@privilege(category: "attachment", privilege: "mutation", domainOwnerGranted: true)'),
|
|
164
|
+
(0, type_graphql_1.Mutation)(returns => thumbnail_backfill_1.ThumbnailBackfillResult, {
|
|
165
|
+
description: '썸네일이 없는 기존 첨부파일들에 대해 서버에서 썸네일을 일괄 생성한다. ' +
|
|
166
|
+
'한 호출당 limit 개까지만 처리하며, remaining > 0 이면 반복 호출 필요.'
|
|
167
|
+
}),
|
|
168
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('limit', type => type_graphql_1.Int, { nullable: true, defaultValue: 20 })),
|
|
169
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
170
|
+
tslib_1.__metadata("design:type", Function),
|
|
171
|
+
tslib_1.__metadata("design:paramtypes", [Number, Object]),
|
|
172
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
173
|
+
], AttachmentMutation.prototype, "backfillAttachmentThumbnails", null);
|
|
156
174
|
exports.AttachmentMutation = AttachmentMutation = tslib_1.__decorate([
|
|
157
175
|
(0, type_graphql_1.Resolver)(attachment_1.Attachment)
|
|
158
176
|
], AttachmentMutation);
|
|
159
177
|
async function createAttachment(_, { attachment }, context) {
|
|
160
178
|
const { file, category, refType = '', refBy, description } = attachment;
|
|
161
|
-
const { mimetype } = (await file);
|
|
179
|
+
const { mimetype: clientMimetype } = (await file);
|
|
180
|
+
const { id, path, size, filename, encoding, contents } = await attachment_const_1.STORAGE.uploadFile({ file, context });
|
|
181
|
+
const { domain, user, tx } = context.state;
|
|
182
|
+
// 클라이언트가 보낸 mimetype 은 OS/브라우저 MIME DB 가 확장자를 인식하지
|
|
183
|
+
// 못하면 'application/octet-stream' 으로 fallback 된다 (특히 .glb). 같은 파일이
|
|
184
|
+
// 환경에 따라 다른 mime 으로 저장되면 썸네일/카테고리/검색 등 후속 로직이
|
|
185
|
+
// 일관성을 잃으므로, 모호한 mime 만 magic byte → 확장자 순으로 재판정한다.
|
|
186
|
+
const mimetype = (0, mimetype_1.normalizeMimetype)(clientMimetype, filename, contents);
|
|
187
|
+
// 허용 mime 검증 — 클라이언트 mime 또는 정규화된 mime 둘 중 하나라도 매칭되면
|
|
188
|
+
// 통과. 클라이언트가 정확한 mime ('model/gltf+json' 등) 을 보낸 케이스와
|
|
189
|
+
// 모호한 mime ('application/octet-stream') 으로 보낸 케이스 둘 다 동일 흐름으로
|
|
190
|
+
// 처리되도록 — 이전엔 STORAGE 저장 전에 client mime 으로만 검증해서 정확한
|
|
191
|
+
// mime 을 보낼수록 까다롭게 거절되는 역설이 있었다.
|
|
192
|
+
// 검증을 STORAGE.uploadFile 후로 옮긴 대신, 거절 시 저장된 파일 cleanup.
|
|
162
193
|
if (allowedMimeTypes instanceof Array && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes('*/*')) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
194
|
+
const matches = (mime) => {
|
|
195
|
+
if (!mime)
|
|
196
|
+
return false;
|
|
197
|
+
return allowedMimeTypes.some(type => {
|
|
198
|
+
const [typeMain, typeSub] = type.split('/');
|
|
199
|
+
const [mimeMain, mimeSub] = mime.split('/');
|
|
200
|
+
return ((typeMain === mimeMain && (typeSub === '*' || typeSub === mimeSub)) || (typeMain === '*' && typeSub === '*'));
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
if (!matches(clientMimetype) && !matches(mimetype)) {
|
|
204
|
+
try {
|
|
205
|
+
await attachment_const_1.STORAGE.deleteFile(path);
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
env_1.logger.warn(`[attachment] cleanup after rejected upload failed: ${e.message}`);
|
|
209
|
+
}
|
|
210
|
+
throw Error(context.t(`error.not allowed file type for upload`, { mimetype: clientMimetype }));
|
|
170
211
|
}
|
|
171
212
|
}
|
|
172
|
-
const
|
|
173
|
-
const { domain, user, tx } = context.state;
|
|
213
|
+
const thumbnail = await (0, thumbnail_1.generateThumbnail)(contents, mimetype);
|
|
174
214
|
const repository = tx ? tx.getRepository(attachment_1.Attachment) : (0, shell_1.getRepository)(attachment_1.Attachment);
|
|
175
215
|
return await repository.save({
|
|
176
216
|
domain,
|
|
@@ -183,10 +223,11 @@ async function createAttachment(_, { attachment }, context) {
|
|
|
183
223
|
encoding,
|
|
184
224
|
refType,
|
|
185
225
|
refBy,
|
|
186
|
-
category: category ||
|
|
226
|
+
category: category || (0, mimetype_1.categoryFromMimetype)(mimetype),
|
|
187
227
|
size: size,
|
|
188
228
|
path,
|
|
189
|
-
contents
|
|
229
|
+
contents,
|
|
230
|
+
thumbnail
|
|
190
231
|
});
|
|
191
232
|
}
|
|
192
233
|
async function createAttachments(_, { attachments }, context) {
|
|
@@ -319,7 +360,7 @@ async function importAttachments(upload, context) {
|
|
|
319
360
|
name,
|
|
320
361
|
mimetype,
|
|
321
362
|
encoding,
|
|
322
|
-
category: category ||
|
|
363
|
+
category: category || (0, mimetype_1.categoryFromMimetype)(mimetype),
|
|
323
364
|
size: size,
|
|
324
365
|
path,
|
|
325
366
|
contents
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-mutation.js","sourceRoot":"","sources":["../../../server/service/attachment/attachment-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { FileUpload } from 'graphql-upload/GraphQLUpload.js'\nimport GraphQLUpload from 'graphql-upload/GraphQLUpload.js'\nimport promisesAll from 'promises-all'\n\nimport { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'\nimport { In } from 'typeorm'\n\nimport { config, logger } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\n\nimport { STORAGE } from '../../attachment-const'\nimport { Attachment } from './attachment'\nimport { AttachmentPatch, NewAttachment, UploadURL } from './attachment-types'\n\nconst allowedMimeTypes = config.get('fileUpload/mimeTypes', [])\n\n@Resolver(Attachment)\nexport class AttachmentMutation {\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async createAttachment(\n @Arg('attachment', type => NewAttachment) attachment: NewAttachment,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n return await createAttachment(null, { attachment }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment])\n async createAttachments(\n @Arg('attachments', type => [NewAttachment]) attachments: NewAttachment[],\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await createAttachments(null, { attachments }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async updateAttachment(\n @Arg('id') id: string,\n @Arg('patch', type => AttachmentPatch) patch: AttachmentPatch,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n const attachment = await getRepository(Attachment).findOne({\n where: {\n domain: { id: context.state.domain.id },\n id\n }\n })\n\n return await getRepository(Attachment).save({\n ...attachment,\n ...patch,\n updater: context.state.user\n })\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean)\n async deleteAttachment(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n return await deleteAttachment(null, { id }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean)\n async deleteAttachmentsByRef(\n @Arg('refBys', type => [String]) refBys: string[],\n @Arg('refType', type => String, { nullable: true }) refType: string,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n return await deleteAttachmentsByRef(null, { refBys, refType }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async singleUpload(\n @Arg('file', type => GraphQLUpload) file: FileUpload,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n return await singleUpload(null, { file }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment])\n async multipleUpload(\n @Arg('files', type => [GraphQLUpload]) files: FileUpload[],\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await multipleUpload(null, { files }, context)\n }\n\n @Mutation(returns => UploadURL)\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n async generateUploadURL(\n @Arg('type', type => String) type: string,\n @Ctx() context: ResolverContext\n ): Promise<{ url: string; fields: { [key: string]: string } }> {\n return await generateUploadURL(null, { type }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment], { description: 'To import some Attachments' })\n async importAttachments(\n @Arg('file', () => GraphQLUpload) file: FileUpload,\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await importAttachments(file, context)\n }\n}\n\nexport async function createAttachment(_: any, { attachment }, context: ResolverContext): Promise<Attachment> {\n const { file, category, refType = '', refBy, description } = attachment\n const { mimetype } = (await file) as FileUpload\n\n if (allowedMimeTypes instanceof Array && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes('*/*')) {\n const isAllowed = allowedMimeTypes.some(type => {\n const [typeMain, typeSub] = type.split('/')\n const [mimeMain, mimeSub] = mimetype.split('/')\n return (\n (typeMain === mimeMain && (typeSub === '*' || typeSub === mimeSub)) || (typeMain === '*' && typeSub === '*')\n )\n })\n\n if (!isAllowed) {\n throw Error(context.t(`error.not allowed file type for upload`, { mimetype }))\n }\n }\n\n const { id, path, size, filename, encoding, contents } = await STORAGE.uploadFile({ file, context })\n const { domain, user, tx } = context.state\n\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n\n return await repository.save({\n domain,\n creator: user,\n updater: user,\n id,\n description,\n name: filename,\n mimetype,\n encoding,\n refType,\n refBy,\n category: category || mimetype.split('/').shift(),\n size: size as any,\n path,\n contents\n })\n}\n\nexport async function createAttachments(_: any, { attachments }, context: ResolverContext): Promise<Attachment[]> {\n const { resolve, reject } = await promisesAll.all(\n attachments.map(attachment => createAttachment(_, { attachment }, context))\n )\n\n if (reject.length) {\n reject.forEach(({ name, message }) => logger.error(`${name}: ${message}`))\n\n return reject\n }\n\n return resolve\n}\n\nexport async function deleteAttachment(_: any, { id }, context: ResolverContext): Promise<boolean> {\n const { domain, tx } = context.state\n\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n const attachment = await repository.findOne({\n where: { domain: { id: domain.id }, id }\n })\n\n if (attachment) {\n await repository.delete({ id: attachment.id })\n await STORAGE.deleteFile(attachment.path)\n return true\n } else {\n return false\n }\n}\n\ninterface DeleteAttachmentsObject {\n refBys: Array<string>\n refType?: string\n}\nexport async function deleteAttachmentsByRef(\n _: any,\n { refBys, refType = null }: DeleteAttachmentsObject,\n context: ResolverContext\n): Promise<boolean> {\n const { domain, tx } = context.state\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n const inquryWhereClause: any = { domain: { id: domain.id }, refBy: In(refBys) }\n const deleteWhereClause: any = { refBy: In(refBys) }\n\n // refType이 존재하면 where 절에 추가\n if (refType) {\n inquryWhereClause.refType = refType\n deleteWhereClause.refType = refType\n }\n\n const attachments = await repository.find({\n where: inquryWhereClause\n })\n\n //remove attachment from repo\n await repository.delete(deleteWhereClause)\n\n //remove files from attachments folder\n if (attachments.length) {\n await Promise.all(\n attachments.map(async attachment => {\n await STORAGE.deleteFile(attachment.path)\n })\n )\n\n return true\n } else {\n return false\n }\n}\n\nexport async function generateUploadURL(\n _: any,\n { type },\n context: ResolverContext\n): Promise<{ url: string; fields: { [key: string]: string } }> {\n return await STORAGE.generateUploadURL(type)\n}\n\nexport async function singleUpload(_: any, { file }, context: ResolverContext): Promise<Attachment> {\n return await createAttachment(null, { attachment: { file } }, context)\n}\n\nexport async function multipleUpload(_: any, { files }, context: ResolverContext): Promise<Attachment[]> {\n return await createAttachments(null, { attachments: { files } }, context)\n}\n\nasync function parseJSONFile(uploadedFile: FileUpload): Promise<any> {\n var { createReadStream } = await uploadedFile\n\n return new Promise((resolve, reject) => {\n const chunks: Uint8Array[] = []\n\n createReadStream()\n .on('data', (chunk: Uint8Array) => {\n chunks.push(chunk)\n })\n .on('end', () => {\n try {\n const fileContents = Buffer.concat(chunks).toString('utf-8')\n const jsonData = JSON.parse(fileContents)\n resolve(jsonData)\n } catch (error) {\n reject(error)\n }\n })\n .on('error', (error: Error) => {\n reject(error)\n })\n })\n}\n\nfunction dataURLToFileUpload(dataURL: string, filename: string, mimeType: string): FileUpload {\n const indexOfComma = dataURL.indexOf(',')\n if (indexOfComma === -1) {\n throw new Error('Invalid Data URL')\n }\n\n const base64Data = dataURL.slice(indexOfComma + 1)\n const buffer = Buffer.from(base64Data, 'base64')\n\n return {\n filename,\n mimetype: mimeType,\n encoding: 'base64',\n createReadStream: () => {\n const stream = require('stream')\n const readable = new stream.Readable()\n readable.push(buffer)\n readable.push(null)\n return readable\n }\n }\n}\n\nexport async function importAttachments(upload: FileUpload, context: ResolverContext): Promise<Attachment[]> {\n const { domain, user, notify, tx } = context.state\n\n const repository = tx.getRepository(Attachment)\n\n const attachments = []\n\n const parsed = await parseJSONFile(upload)\n\n for (const id in parsed) {\n var { name, description, category, mimetype, encoding, contents } = parsed[id]\n if (!name || !contents || !mimetype) {\n throw 'Malformed attachments import file'\n }\n\n var sameIdAttachment = await repository.findOneBy({ id })\n\n if (sameIdAttachment) {\n if (sameIdAttachment.domainId != domain.id) {\n throw `Attachment with id/name(${id}/${name}) is already taken in another domain`\n }\n\n // 동일 아이디 첨부파일이 있다면, 스킵한다.\n continue\n }\n\n const file = dataURLToFileUpload(contents, name, mimetype)\n\n var { path, size, contents } = await STORAGE.uploadFile({ id, file, context })\n\n attachments.push(\n await repository.save({\n domain,\n creator: user,\n updater: user,\n id,\n description,\n name,\n mimetype,\n encoding,\n category: category || mimetype.split('/').shift(),\n size: size as any,\n path,\n contents\n })\n )\n }\n\n notify &&\n notify({\n mode: 'in-app',\n title: `${attachments.length} Attachment(s) are imported`,\n body: `${attachments.length} Attachment(s) are imported by ${user.name}`\n })\n\n return attachments\n}\n"]}
|
|
1
|
+
{"version":3,"file":"attachment-mutation.js","sourceRoot":"","sources":["../../../server/service/attachment/attachment-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { FileUpload } from 'graphql-upload/GraphQLUpload.js'\nimport GraphQLUpload from 'graphql-upload/GraphQLUpload.js'\nimport promisesAll from 'promises-all'\n\nimport { Arg, Ctx, Directive, Int, Mutation, Resolver } from 'type-graphql'\nimport { In } from 'typeorm'\n\nimport { config, logger } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\n\nimport { STORAGE } from '../../attachment-const'\nimport { Attachment } from './attachment'\nimport { AttachmentPatch, NewAttachment, UploadURL } from './attachment-types'\nimport { categoryFromMimetype, normalizeMimetype } from './mimetype'\nimport { generateThumbnail } from './thumbnail'\nimport { backfillAttachmentThumbnails, ThumbnailBackfillResult } from './thumbnail-backfill'\n\nconst allowedMimeTypes = config.get('fileUpload/mimeTypes', [])\n\n@Resolver(Attachment)\nexport class AttachmentMutation {\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async createAttachment(\n @Arg('attachment', type => NewAttachment) attachment: NewAttachment,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n return await createAttachment(null, { attachment }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment])\n async createAttachments(\n @Arg('attachments', type => [NewAttachment]) attachments: NewAttachment[],\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await createAttachments(null, { attachments }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async updateAttachment(\n @Arg('id') id: string,\n @Arg('patch', type => AttachmentPatch) patch: AttachmentPatch,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n const attachment = await getRepository(Attachment).findOne({\n where: {\n domain: { id: context.state.domain.id },\n id\n }\n })\n\n return await getRepository(Attachment).save({\n ...attachment,\n ...patch,\n updater: context.state.user\n })\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean)\n async deleteAttachment(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n return await deleteAttachment(null, { id }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean)\n async deleteAttachmentsByRef(\n @Arg('refBys', type => [String]) refBys: string[],\n @Arg('refType', type => String, { nullable: true }) refType: string,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n return await deleteAttachmentsByRef(null, { refBys, refType }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async singleUpload(\n @Arg('file', type => GraphQLUpload) file: FileUpload,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n return await singleUpload(null, { file }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment])\n async multipleUpload(\n @Arg('files', type => [GraphQLUpload]) files: FileUpload[],\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await multipleUpload(null, { files }, context)\n }\n\n @Mutation(returns => UploadURL)\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n async generateUploadURL(\n @Arg('type', type => String) type: string,\n @Ctx() context: ResolverContext\n ): Promise<{ url: string; fields: { [key: string]: string } }> {\n return await generateUploadURL(null, { type }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment], { description: 'To import some Attachments' })\n async importAttachments(\n @Arg('file', () => GraphQLUpload) file: FileUpload,\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await importAttachments(file, context)\n }\n\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ThumbnailBackfillResult, {\n description:\n '썸네일이 없는 기존 첨부파일들에 대해 서버에서 썸네일을 일괄 생성한다. ' +\n '한 호출당 limit 개까지만 처리하며, remaining > 0 이면 반복 호출 필요.'\n })\n async backfillAttachmentThumbnails(\n @Arg('limit', type => Int, { nullable: true, defaultValue: 20 }) limit: number,\n @Ctx() context: ResolverContext\n ): Promise<ThumbnailBackfillResult> {\n return await backfillAttachmentThumbnails({ limit }, context)\n }\n}\n\nexport async function createAttachment(_: any, { attachment }, context: ResolverContext): Promise<Attachment> {\n const { file, category, refType = '', refBy, description } = attachment\n const { mimetype: clientMimetype } = (await file) as FileUpload\n\n const { id, path, size, filename, encoding, contents } = await STORAGE.uploadFile({ file, context })\n const { domain, user, tx } = context.state\n\n // 클라이언트가 보낸 mimetype 은 OS/브라우저 MIME DB 가 확장자를 인식하지\n // 못하면 'application/octet-stream' 으로 fallback 된다 (특히 .glb). 같은 파일이\n // 환경에 따라 다른 mime 으로 저장되면 썸네일/카테고리/검색 등 후속 로직이\n // 일관성을 잃으므로, 모호한 mime 만 magic byte → 확장자 순으로 재판정한다.\n const mimetype = normalizeMimetype(clientMimetype, filename, contents)\n\n // 허용 mime 검증 — 클라이언트 mime 또는 정규화된 mime 둘 중 하나라도 매칭되면\n // 통과. 클라이언트가 정확한 mime ('model/gltf+json' 등) 을 보낸 케이스와\n // 모호한 mime ('application/octet-stream') 으로 보낸 케이스 둘 다 동일 흐름으로\n // 처리되도록 — 이전엔 STORAGE 저장 전에 client mime 으로만 검증해서 정확한\n // mime 을 보낼수록 까다롭게 거절되는 역설이 있었다.\n // 검증을 STORAGE.uploadFile 후로 옮긴 대신, 거절 시 저장된 파일 cleanup.\n if (allowedMimeTypes instanceof Array && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes('*/*')) {\n const matches = (mime: string) => {\n if (!mime) return false\n return allowedMimeTypes.some(type => {\n const [typeMain, typeSub] = type.split('/')\n const [mimeMain, mimeSub] = mime.split('/')\n return (\n (typeMain === mimeMain && (typeSub === '*' || typeSub === mimeSub)) || (typeMain === '*' && typeSub === '*')\n )\n })\n }\n\n if (!matches(clientMimetype) && !matches(mimetype)) {\n try { await STORAGE.deleteFile(path) } catch (e) {\n logger.warn(`[attachment] cleanup after rejected upload failed: ${(e as Error).message}`)\n }\n throw Error(context.t(`error.not allowed file type for upload`, { mimetype: clientMimetype }))\n }\n }\n\n const thumbnail = await generateThumbnail(contents, mimetype)\n\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n\n return await repository.save({\n domain,\n creator: user,\n updater: user,\n id,\n description,\n name: filename,\n mimetype,\n encoding,\n refType,\n refBy,\n category: category || categoryFromMimetype(mimetype),\n size: size as any,\n path,\n contents,\n thumbnail\n })\n}\n\nexport async function createAttachments(_: any, { attachments }, context: ResolverContext): Promise<Attachment[]> {\n const { resolve, reject } = await promisesAll.all(\n attachments.map(attachment => createAttachment(_, { attachment }, context))\n )\n\n if (reject.length) {\n reject.forEach(({ name, message }) => logger.error(`${name}: ${message}`))\n\n return reject\n }\n\n return resolve\n}\n\nexport async function deleteAttachment(_: any, { id }, context: ResolverContext): Promise<boolean> {\n const { domain, tx } = context.state\n\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n const attachment = await repository.findOne({\n where: { domain: { id: domain.id }, id }\n })\n\n if (attachment) {\n await repository.delete({ id: attachment.id })\n await STORAGE.deleteFile(attachment.path)\n return true\n } else {\n return false\n }\n}\n\ninterface DeleteAttachmentsObject {\n refBys: Array<string>\n refType?: string\n}\nexport async function deleteAttachmentsByRef(\n _: any,\n { refBys, refType = null }: DeleteAttachmentsObject,\n context: ResolverContext\n): Promise<boolean> {\n const { domain, tx } = context.state\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n const inquryWhereClause: any = { domain: { id: domain.id }, refBy: In(refBys) }\n const deleteWhereClause: any = { refBy: In(refBys) }\n\n // refType이 존재하면 where 절에 추가\n if (refType) {\n inquryWhereClause.refType = refType\n deleteWhereClause.refType = refType\n }\n\n const attachments = await repository.find({\n where: inquryWhereClause\n })\n\n //remove attachment from repo\n await repository.delete(deleteWhereClause)\n\n //remove files from attachments folder\n if (attachments.length) {\n await Promise.all(\n attachments.map(async attachment => {\n await STORAGE.deleteFile(attachment.path)\n })\n )\n\n return true\n } else {\n return false\n }\n}\n\nexport async function generateUploadURL(\n _: any,\n { type },\n context: ResolverContext\n): Promise<{ url: string; fields: { [key: string]: string } }> {\n return await STORAGE.generateUploadURL(type)\n}\n\nexport async function singleUpload(_: any, { file }, context: ResolverContext): Promise<Attachment> {\n return await createAttachment(null, { attachment: { file } }, context)\n}\n\nexport async function multipleUpload(_: any, { files }, context: ResolverContext): Promise<Attachment[]> {\n return await createAttachments(null, { attachments: { files } }, context)\n}\n\nasync function parseJSONFile(uploadedFile: FileUpload): Promise<any> {\n var { createReadStream } = await uploadedFile\n\n return new Promise((resolve, reject) => {\n const chunks: Uint8Array[] = []\n\n createReadStream()\n .on('data', (chunk: Uint8Array) => {\n chunks.push(chunk)\n })\n .on('end', () => {\n try {\n const fileContents = Buffer.concat(chunks).toString('utf-8')\n const jsonData = JSON.parse(fileContents)\n resolve(jsonData)\n } catch (error) {\n reject(error)\n }\n })\n .on('error', (error: Error) => {\n reject(error)\n })\n })\n}\n\nfunction dataURLToFileUpload(dataURL: string, filename: string, mimeType: string): FileUpload {\n const indexOfComma = dataURL.indexOf(',')\n if (indexOfComma === -1) {\n throw new Error('Invalid Data URL')\n }\n\n const base64Data = dataURL.slice(indexOfComma + 1)\n const buffer = Buffer.from(base64Data, 'base64')\n\n return {\n filename,\n mimetype: mimeType,\n encoding: 'base64',\n createReadStream: () => {\n const stream = require('stream')\n const readable = new stream.Readable()\n readable.push(buffer)\n readable.push(null)\n return readable\n }\n }\n}\n\nexport async function importAttachments(upload: FileUpload, context: ResolverContext): Promise<Attachment[]> {\n const { domain, user, notify, tx } = context.state\n\n const repository = tx.getRepository(Attachment)\n\n const attachments = []\n\n const parsed = await parseJSONFile(upload)\n\n for (const id in parsed) {\n var { name, description, category, mimetype, encoding, contents } = parsed[id]\n if (!name || !contents || !mimetype) {\n throw 'Malformed attachments import file'\n }\n\n var sameIdAttachment = await repository.findOneBy({ id })\n\n if (sameIdAttachment) {\n if (sameIdAttachment.domainId != domain.id) {\n throw `Attachment with id/name(${id}/${name}) is already taken in another domain`\n }\n\n // 동일 아이디 첨부파일이 있다면, 스킵한다.\n continue\n }\n\n const file = dataURLToFileUpload(contents, name, mimetype)\n\n var { path, size, contents } = await STORAGE.uploadFile({ id, file, context })\n\n attachments.push(\n await repository.save({\n domain,\n creator: user,\n updater: user,\n id,\n description,\n name,\n mimetype,\n encoding,\n category: category || categoryFromMimetype(mimetype),\n size: size as any,\n path,\n contents\n })\n )\n }\n\n notify &&\n notify({\n mode: 'in-app',\n title: `${attachments.length} Attachment(s) are imported`,\n body: `${attachments.length} Attachment(s) are imported by ${user.name}`\n })\n\n return attachments\n}\n"]}
|
|
@@ -14,6 +14,13 @@ export declare class Attachment {
|
|
|
14
14
|
path?: string;
|
|
15
15
|
size?: string;
|
|
16
16
|
contents?: Buffer;
|
|
17
|
+
/**
|
|
18
|
+
* 썸네일 이미지 리소스 URL. 두 가지 형태 모두 유효:
|
|
19
|
+
* - 내부 생성 썸네일: `data:image/jpeg;base64,...` (업로드 시 자동 생성)
|
|
20
|
+
* - 외부 리소스 참조: `https://cdn.example.com/thumb.jpg`
|
|
21
|
+
* 둘 다 `<img src>` 에 그대로 사용 가능.
|
|
22
|
+
*/
|
|
23
|
+
thumbnail?: string;
|
|
17
24
|
tags?: string[];
|
|
18
25
|
createdAt?: Date;
|
|
19
26
|
updatedAt?: Date;
|
|
@@ -93,8 +93,20 @@ tslib_1.__decorate([
|
|
|
93
93
|
: 'blob',
|
|
94
94
|
length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
|
|
95
95
|
}),
|
|
96
|
-
tslib_1.__metadata("design:type", Buffer
|
|
96
|
+
tslib_1.__metadata("design:type", Buffer
|
|
97
|
+
/**
|
|
98
|
+
* 썸네일 이미지 리소스 URL. 두 가지 형태 모두 유효:
|
|
99
|
+
* - 내부 생성 썸네일: `data:image/jpeg;base64,...` (업로드 시 자동 생성)
|
|
100
|
+
* - 외부 리소스 참조: `https://cdn.example.com/thumb.jpg`
|
|
101
|
+
* 둘 다 `<img src>` 에 그대로 사용 가능.
|
|
102
|
+
*/
|
|
103
|
+
)
|
|
97
104
|
], Attachment.prototype, "contents", void 0);
|
|
105
|
+
tslib_1.__decorate([
|
|
106
|
+
(0, typeorm_1.Column)({ type: 'text', nullable: true }),
|
|
107
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
108
|
+
tslib_1.__metadata("design:type", String)
|
|
109
|
+
], Attachment.prototype, "thumbnail", void 0);
|
|
98
110
|
tslib_1.__decorate([
|
|
99
111
|
(0, typeorm_1.Column)('simple-json', { nullable: true, default: null }),
|
|
100
112
|
(0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment.js","sourceRoot":"","sources":["../../../server/service/attachment/attachment.ts"],"names":[],"mappings":";;;;AAAA,+CAAoD;AACpD,qCASgB;AAEhB,yDAAgD;AAChD,iDAA4D;AAE5D,6DAAwD;AACxD,6CAA4C;AAE5C,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AAC7C,MAAM,aAAa,GAAG,SAAS,CAAC,IAAI,CAAA;AAc7B,IAAM,UAAU,GAAhB,MAAM,UAAU;IAAhB;QAkCL,YAAO,GAAY,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"attachment.js","sourceRoot":"","sources":["../../../server/service/attachment/attachment.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, ID, ObjectType } from 'type-graphql'\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\n\nimport { User } from '@things-factory/auth-base'\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\nimport { ATTACHMENT_PATH } from '../../attachment-const'\nimport { config } from '@things-factory/env'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n@Index('ix_attachment_0', (attachment: Attachment) => [attachment.domain, attachment.name], { unique: false })\n@Index('ix_attachment_1', (attachment: Attachment) => [attachment.domain, attachment.category, attachment.name], {\n unique: false\n})\n@Index('ix_attachment_2', (attachment: Attachment) => [attachment.domain, attachment.refBy], {\n unique: false\n})\n@Index('ix_attachment_3', (attachment: Attachment) => [attachment.domain, attachment.refType, attachment.refBy], {\n unique: false\n})\n@ObjectType()\nexport class Attachment {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id?: string\n\n @ManyToOne(type => Domain, { nullable: false })\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((attachment: Attachment) => attachment.domain)\n domainId?: string\n\n @Column()\n @Field()\n name?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n @Column()\n @Field()\n mimetype?: string\n\n @Column()\n @Field()\n encoding?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n category?: string\n\n @Column({ nullable: true, default: '' })\n @Field({ nullable: true })\n refType?: string = ''\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n refBy?: string\n\n @Column()\n @Field()\n path?: string\n\n @Column({\n nullable: true,\n type: DATABASE_TYPE == 'mssql' ? 'bigint' : undefined\n })\n @Field()\n size?: string\n\n @Column({\n nullable: true,\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longblob'\n : DATABASE_TYPE == 'postgres'\n ? 'bytea'\n : DATABASE_TYPE == 'mssql'\n ? 'varbinary'\n : 'blob',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n contents?: Buffer\n\n /**\n * 썸네일 이미지 리소스 URL. 두 가지 형태 모두 유효:\n * - 내부 생성 썸네일: `data:image/jpeg;base64,...` (업로드 시 자동 생성)\n * - 외부 리소스 참조: `https://cdn.example.com/thumb.jpg`\n * 둘 다 `<img src>` 에 그대로 사용 가능.\n */\n @Column({ type: 'text', nullable: true })\n @Field({ nullable: true })\n thumbnail?: string\n\n @Column('simple-json', { nullable: true, default: null })\n @Field(type => ScalarObject, { nullable: true })\n tags?: string[]\n\n @CreateDateColumn()\n @Field()\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field()\n updatedAt?: Date\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((attachment: Attachment) => attachment.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((attachment: Attachment) => attachment.updater)\n updaterId?: string\n\n @Field()\n get fullpath(): string {\n return `/${ATTACHMENT_PATH}/${this.path}`\n }\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLTF/GLB 바이트로부터 정적 JPEG 썸네일 data URI 생성.
|
|
3
|
+
* headless Chrome 에 Three.js 로 렌더링된 단일 프레임을 캡처한다.
|
|
4
|
+
* 지원 타입: model/gltf-binary (.glb), model/gltf+json (.gltf)
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateGltfThumbnail(contents: Buffer | undefined | null, mimetype: string | undefined): Promise<string | undefined>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateGltfThumbnail = generateGltfThumbnail;
|
|
4
|
+
const env_1 = require("@things-factory/env");
|
|
5
|
+
const shell_1 = require("@things-factory/shell");
|
|
6
|
+
/** 첨부파일 썸네일 전용 headless pool — 보드 pool 과 격리 */
|
|
7
|
+
const POOL_NAME = 'attachment-thumbnail';
|
|
8
|
+
const W = 400;
|
|
9
|
+
const H = 400;
|
|
10
|
+
// AWS EC2 일반 인스턴스 (GPU 없음) 환경에서는 swiftshader software 렌더링이라
|
|
11
|
+
// Three.js + GLTFLoader 가 15s 안에 끝나기 어렵다. 첫 호출의 cold start (browser
|
|
12
|
+
// launch + asset fetch + parse + render) 까지 흡수할 수 있게 30s 로 상향.
|
|
13
|
+
const TIMEOUT_MS = 30000;
|
|
14
|
+
/**
|
|
15
|
+
* 1×1 흰색 PNG — 외부 텍스처 fetch 실패 시 placeholder 로 응답하기 위한 바이트.
|
|
16
|
+
* GLTFLoader 는 텍스처 reference 가 부재하면 promise 를 reject 하므로, "텍스처
|
|
17
|
+
* 못 가져오면 그냥 흰색으로라도 진행" 으로 동작시키면 모델 형태만이라도 썸네일에
|
|
18
|
+
* 남는다 ("썸네일 미생성" 보다 명백히 우월).
|
|
19
|
+
*/
|
|
20
|
+
const TINY_PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64');
|
|
21
|
+
/**
|
|
22
|
+
* 약속: 아래 virtual URL 들은 request interception 으로만 의미를 가짐.
|
|
23
|
+
* - /__viewer__ : 뷰어 HTML
|
|
24
|
+
* - /__gltf__ : GLTF 바이트 (binary) 또는 JSON (text)
|
|
25
|
+
* puppeteer page 가 내부 네비게이션/fetch 시 이 URL 들을 만나면 mock response.
|
|
26
|
+
*/
|
|
27
|
+
const VIEWER_URL = 'http://attachment-thumbnail.local/__viewer__';
|
|
28
|
+
const GLTF_URL = 'http://attachment-thumbnail.local/__gltf__';
|
|
29
|
+
const VIEWER_HTML = `<!doctype html>
|
|
30
|
+
<html><head>
|
|
31
|
+
<meta charset="utf-8">
|
|
32
|
+
<style>
|
|
33
|
+
html, body { margin: 0; padding: 0; background: #f5f5f5; }
|
|
34
|
+
canvas { display: block; }
|
|
35
|
+
</style>
|
|
36
|
+
<script type="importmap">
|
|
37
|
+
{
|
|
38
|
+
"imports": {
|
|
39
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.183.2/build/three.module.js",
|
|
40
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<script type="module">
|
|
47
|
+
import * as THREE from 'three'
|
|
48
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
|
|
49
|
+
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
|
|
50
|
+
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'
|
|
51
|
+
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'
|
|
52
|
+
|
|
53
|
+
const W = ${W}, H = ${H}
|
|
54
|
+
window.__gltfReady = false
|
|
55
|
+
window.__gltfError = null
|
|
56
|
+
|
|
57
|
+
// AWS EC2 일반 인스턴스 (GPU 없음) 환경에서는 swiftshader software 렌더링이라
|
|
58
|
+
// antialias / 높은 pixelRatio 가 매우 비싸다. 정적 썸네일 용도라 antialias 끄고
|
|
59
|
+
// pixelRatio 1 로 고정.
|
|
60
|
+
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: false })
|
|
61
|
+
renderer.setPixelRatio(1)
|
|
62
|
+
renderer.setSize(W, H)
|
|
63
|
+
renderer.setClearColor(0xf5f5f5, 1)
|
|
64
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace
|
|
65
|
+
document.body.appendChild(renderer.domElement)
|
|
66
|
+
|
|
67
|
+
const scene = new THREE.Scene()
|
|
68
|
+
const camera = new THREE.PerspectiveCamera(35, W / H, 0.1, 10000)
|
|
69
|
+
|
|
70
|
+
// 기본 조명 — 모델 세부 손실 최소화
|
|
71
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.6))
|
|
72
|
+
const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8)
|
|
73
|
+
scene.add(hemi)
|
|
74
|
+
const dir = new THREE.DirectionalLight(0xffffff, 1.2)
|
|
75
|
+
dir.position.set(5, 10, 7.5)
|
|
76
|
+
scene.add(dir)
|
|
77
|
+
|
|
78
|
+
;(async () => {
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch('${GLTF_URL}')
|
|
81
|
+
if (!res.ok) throw new Error('fetch failed: ' + res.status)
|
|
82
|
+
const ab = await res.arrayBuffer()
|
|
83
|
+
|
|
84
|
+
const loader = new GLTFLoader()
|
|
85
|
+
|
|
86
|
+
// DRACO 압축 메쉬 지원 (필요한 GLB 만 로드 시 decoder fetch)
|
|
87
|
+
const draco = new DRACOLoader()
|
|
88
|
+
draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/draco/')
|
|
89
|
+
loader.setDRACOLoader(draco)
|
|
90
|
+
|
|
91
|
+
// KTX2 (BasisU) 압축 텍스처 지원
|
|
92
|
+
const ktx2 = new KTX2Loader()
|
|
93
|
+
ktx2.setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/basis/')
|
|
94
|
+
ktx2.detectSupport(renderer)
|
|
95
|
+
loader.setKTX2Loader(ktx2)
|
|
96
|
+
|
|
97
|
+
// Meshopt 압축 지원
|
|
98
|
+
loader.setMeshoptDecoder(MeshoptDecoder)
|
|
99
|
+
|
|
100
|
+
const gltf = await loader.parseAsync(ab, '')
|
|
101
|
+
scene.add(gltf.scene)
|
|
102
|
+
|
|
103
|
+
// Bounding box 기반 카메라 auto-fit
|
|
104
|
+
const box = new THREE.Box3().setFromObject(gltf.scene)
|
|
105
|
+
const size = box.getSize(new THREE.Vector3())
|
|
106
|
+
const center = box.getCenter(new THREE.Vector3())
|
|
107
|
+
const maxDim = Math.max(size.x, size.y, size.z) || 1
|
|
108
|
+
const fov = camera.fov * (Math.PI / 180)
|
|
109
|
+
const distance = (maxDim / 2) / Math.tan(fov / 2) * 2.0
|
|
110
|
+
|
|
111
|
+
// 45도 정면-위쪽 뷰포인트 — 3D 모델 특징이 잘 드러나는 각도
|
|
112
|
+
const dirVec = new THREE.Vector3(1, 0.7, 1).normalize()
|
|
113
|
+
camera.position.copy(center).add(dirVec.multiplyScalar(distance))
|
|
114
|
+
camera.near = Math.max(distance / 100, 0.01)
|
|
115
|
+
camera.far = distance * 10
|
|
116
|
+
camera.updateProjectionMatrix()
|
|
117
|
+
camera.lookAt(center)
|
|
118
|
+
|
|
119
|
+
// 단일 프레임 렌더 (정적 썸네일이라 두 번 render + rAF 불필요)
|
|
120
|
+
renderer.render(scene, camera)
|
|
121
|
+
window.__gltfReady = true
|
|
122
|
+
} catch (e) {
|
|
123
|
+
window.__gltfError = e?.message || String(e)
|
|
124
|
+
}
|
|
125
|
+
})()
|
|
126
|
+
</script>
|
|
127
|
+
</body></html>`;
|
|
128
|
+
/**
|
|
129
|
+
* GLTF/GLB 바이트로부터 정적 JPEG 썸네일 data URI 생성.
|
|
130
|
+
* headless Chrome 에 Three.js 로 렌더링된 단일 프레임을 캡처한다.
|
|
131
|
+
* 지원 타입: model/gltf-binary (.glb), model/gltf+json (.gltf)
|
|
132
|
+
*/
|
|
133
|
+
async function generateGltfThumbnail(contents, mimetype) {
|
|
134
|
+
if (!contents)
|
|
135
|
+
return;
|
|
136
|
+
if (mimetype !== 'model/gltf-binary' && mimetype !== 'model/gltf+json')
|
|
137
|
+
return;
|
|
138
|
+
let pool;
|
|
139
|
+
try {
|
|
140
|
+
pool = (0, shell_1.getOrCreateHeadlessPool)(POOL_NAME, { min: 0, max: 2 });
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
env_1.logger.warn(`[attachment] gltf thumbnail — headless pool unavailable: ${e.message}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const browser = await pool.acquire();
|
|
147
|
+
if (!browser)
|
|
148
|
+
return;
|
|
149
|
+
const page = await browser.newPage();
|
|
150
|
+
try {
|
|
151
|
+
await page.setViewport({ width: W, height: H, deviceScaleFactor: 1 });
|
|
152
|
+
await page.setRequestInterception(true);
|
|
153
|
+
page.on('request', async (req) => {
|
|
154
|
+
const url = req.url();
|
|
155
|
+
if (url === VIEWER_URL) {
|
|
156
|
+
await req.respond({ status: 200, contentType: 'text/html; charset=utf-8', body: VIEWER_HTML });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (url === GLTF_URL) {
|
|
160
|
+
await req.respond({
|
|
161
|
+
status: 200,
|
|
162
|
+
contentType: mimetype === 'model/gltf+json' ? 'model/gltf+json' : 'application/octet-stream',
|
|
163
|
+
body: contents
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// 외부 텍스처 (.png/.jpg/.webp/.gif/.ktx2/.basis) 는 1x1 placeholder 로
|
|
168
|
+
// 응답한다. GLB 는 보통 self-contained 라 외부 fetch 가 거의 없지만,
|
|
169
|
+
// gltf+json 은 외부 텍스처 reference 가 흔하고 그 fetch 가 실패하면
|
|
170
|
+
// GLTFLoader 가 promise reject → 썸네일 미생성으로 이어졌다. 텍스처
|
|
171
|
+
// 누락은 무시하고 모델 형태만이라도 캡처하도록 placeholder 응답.
|
|
172
|
+
// (.bin 외부 buffer 와 wasm decoder 는 본질적 한계 — 그대로 통과.)
|
|
173
|
+
if (/\.(png|jpe?g|webp|gif|ktx2|basis)(\?|$)/i.test(url)) {
|
|
174
|
+
await req.respond({ status: 200, contentType: 'image/png', body: TINY_PNG });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
req.continue();
|
|
178
|
+
});
|
|
179
|
+
await page.goto(VIEWER_URL, { waitUntil: 'domcontentloaded' });
|
|
180
|
+
await page.waitForFunction('window.__gltfReady === true || window.__gltfError', {
|
|
181
|
+
timeout: TIMEOUT_MS
|
|
182
|
+
});
|
|
183
|
+
const err = await page.evaluate('window.__gltfError');
|
|
184
|
+
if (err) {
|
|
185
|
+
env_1.logger.warn(`[attachment] gltf thumbnail error: ${err}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// puppeteer 신버전은 Uint8Array 를 반환하는데 Uint8Array.toString('base64') 는 encoding 인자를
|
|
189
|
+
// 무시해서 decimal comma string 이 나온다. encoding: 'base64' 를 직접 요청해서 문자열을 받는다.
|
|
190
|
+
const b64 = (await page.screenshot({ type: 'jpeg', quality: 70, encoding: 'base64' }));
|
|
191
|
+
return `data:image/jpeg;base64,${b64}`;
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
env_1.logger.warn(`[attachment] gltf thumbnail failed: ${e.message}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
try {
|
|
199
|
+
await page.close();
|
|
200
|
+
}
|
|
201
|
+
catch { }
|
|
202
|
+
try {
|
|
203
|
+
await pool.release(browser);
|
|
204
|
+
}
|
|
205
|
+
catch { }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=gltf-thumbnail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gltf-thumbnail.js","sourceRoot":"","sources":["../../../server/service/attachment/gltf-thumbnail.ts"],"names":[],"mappings":"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","sourcesContent":["import { logger } from '@things-factory/env'\nimport { getOrCreateHeadlessPool } from '@things-factory/shell'\n\n/** 첨부파일 썸네일 전용 headless pool — 보드 pool 과 격리 */\nconst POOL_NAME = 'attachment-thumbnail'\n\nconst W = 400\nconst H = 400\n// AWS EC2 일반 인스턴스 (GPU 없음) 환경에서는 swiftshader software 렌더링이라\n// Three.js + GLTFLoader 가 15s 안에 끝나기 어렵다. 첫 호출의 cold start (browser\n// launch + asset fetch + parse + render) 까지 흡수할 수 있게 30s 로 상향.\nconst TIMEOUT_MS = 30000\n\n/**\n * 1×1 흰색 PNG — 외부 텍스처 fetch 실패 시 placeholder 로 응답하기 위한 바이트.\n * GLTFLoader 는 텍스처 reference 가 부재하면 promise 를 reject 하므로, \"텍스처\n * 못 가져오면 그냥 흰색으로라도 진행\" 으로 동작시키면 모델 형태만이라도 썸네일에\n * 남는다 (\"썸네일 미생성\" 보다 명백히 우월).\n */\nconst TINY_PNG = Buffer.from(\n 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',\n 'base64'\n)\n\n/**\n * 약속: 아래 virtual URL 들은 request interception 으로만 의미를 가짐.\n * - /__viewer__ : 뷰어 HTML\n * - /__gltf__ : GLTF 바이트 (binary) 또는 JSON (text)\n * puppeteer page 가 내부 네비게이션/fetch 시 이 URL 들을 만나면 mock response.\n */\nconst VIEWER_URL = 'http://attachment-thumbnail.local/__viewer__'\nconst GLTF_URL = 'http://attachment-thumbnail.local/__gltf__'\n\nconst VIEWER_HTML = `<!doctype html>\n<html><head>\n<meta charset=\"utf-8\">\n<style>\n html, body { margin: 0; padding: 0; background: #f5f5f5; }\n canvas { display: block; }\n</style>\n<script type=\"importmap\">\n{\n \"imports\": {\n \"three\": \"https://cdn.jsdelivr.net/npm/three@0.183.2/build/three.module.js\",\n \"three/addons/\": \"https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/\"\n }\n}\n</script>\n</head>\n<body>\n<script type=\"module\">\nimport * as THREE from 'three'\nimport { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'\nimport { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'\nimport { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'\nimport { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'\n\nconst W = ${W}, H = ${H}\nwindow.__gltfReady = false\nwindow.__gltfError = null\n\n// AWS EC2 일반 인스턴스 (GPU 없음) 환경에서는 swiftshader software 렌더링이라\n// antialias / 높은 pixelRatio 가 매우 비싸다. 정적 썸네일 용도라 antialias 끄고\n// pixelRatio 1 로 고정.\nconst renderer = new THREE.WebGLRenderer({ alpha: true, antialias: false })\nrenderer.setPixelRatio(1)\nrenderer.setSize(W, H)\nrenderer.setClearColor(0xf5f5f5, 1)\nrenderer.outputColorSpace = THREE.SRGBColorSpace\ndocument.body.appendChild(renderer.domElement)\n\nconst scene = new THREE.Scene()\nconst camera = new THREE.PerspectiveCamera(35, W / H, 0.1, 10000)\n\n// 기본 조명 — 모델 세부 손실 최소화\nscene.add(new THREE.AmbientLight(0xffffff, 0.6))\nconst hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8)\nscene.add(hemi)\nconst dir = new THREE.DirectionalLight(0xffffff, 1.2)\ndir.position.set(5, 10, 7.5)\nscene.add(dir)\n\n;(async () => {\n try {\n const res = await fetch('${GLTF_URL}')\n if (!res.ok) throw new Error('fetch failed: ' + res.status)\n const ab = await res.arrayBuffer()\n\n const loader = new GLTFLoader()\n\n // DRACO 압축 메쉬 지원 (필요한 GLB 만 로드 시 decoder fetch)\n const draco = new DRACOLoader()\n draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/draco/')\n loader.setDRACOLoader(draco)\n\n // KTX2 (BasisU) 압축 텍스처 지원\n const ktx2 = new KTX2Loader()\n ktx2.setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/basis/')\n ktx2.detectSupport(renderer)\n loader.setKTX2Loader(ktx2)\n\n // Meshopt 압축 지원\n loader.setMeshoptDecoder(MeshoptDecoder)\n\n const gltf = await loader.parseAsync(ab, '')\n scene.add(gltf.scene)\n\n // Bounding box 기반 카메라 auto-fit\n const box = new THREE.Box3().setFromObject(gltf.scene)\n const size = box.getSize(new THREE.Vector3())\n const center = box.getCenter(new THREE.Vector3())\n const maxDim = Math.max(size.x, size.y, size.z) || 1\n const fov = camera.fov * (Math.PI / 180)\n const distance = (maxDim / 2) / Math.tan(fov / 2) * 2.0\n\n // 45도 정면-위쪽 뷰포인트 — 3D 모델 특징이 잘 드러나는 각도\n const dirVec = new THREE.Vector3(1, 0.7, 1).normalize()\n camera.position.copy(center).add(dirVec.multiplyScalar(distance))\n camera.near = Math.max(distance / 100, 0.01)\n camera.far = distance * 10\n camera.updateProjectionMatrix()\n camera.lookAt(center)\n\n // 단일 프레임 렌더 (정적 썸네일이라 두 번 render + rAF 불필요)\n renderer.render(scene, camera)\n window.__gltfReady = true\n } catch (e) {\n window.__gltfError = e?.message || String(e)\n }\n})()\n</script>\n</body></html>`\n\n/**\n * GLTF/GLB 바이트로부터 정적 JPEG 썸네일 data URI 생성.\n * headless Chrome 에 Three.js 로 렌더링된 단일 프레임을 캡처한다.\n * 지원 타입: model/gltf-binary (.glb), model/gltf+json (.gltf)\n */\nexport async function generateGltfThumbnail(\n contents: Buffer | undefined | null,\n mimetype: string | undefined\n): Promise<string | undefined> {\n if (!contents) return\n if (mimetype !== 'model/gltf-binary' && mimetype !== 'model/gltf+json') return\n\n let pool: any\n try {\n pool = getOrCreateHeadlessPool(POOL_NAME, { min: 0, max: 2 })\n } catch (e) {\n logger.warn(`[attachment] gltf thumbnail — headless pool unavailable: ${(e as Error).message}`)\n return\n }\n\n const browser = await pool.acquire()\n if (!browser) return\n\n const page = await browser.newPage()\n try {\n await page.setViewport({ width: W, height: H, deviceScaleFactor: 1 })\n await page.setRequestInterception(true)\n\n page.on('request', async (req: any) => {\n const url = req.url()\n if (url === VIEWER_URL) {\n await req.respond({ status: 200, contentType: 'text/html; charset=utf-8', body: VIEWER_HTML })\n return\n }\n if (url === GLTF_URL) {\n await req.respond({\n status: 200,\n contentType: mimetype === 'model/gltf+json' ? 'model/gltf+json' : 'application/octet-stream',\n body: contents\n })\n return\n }\n // 외부 텍스처 (.png/.jpg/.webp/.gif/.ktx2/.basis) 는 1x1 placeholder 로\n // 응답한다. GLB 는 보통 self-contained 라 외부 fetch 가 거의 없지만,\n // gltf+json 은 외부 텍스처 reference 가 흔하고 그 fetch 가 실패하면\n // GLTFLoader 가 promise reject → 썸네일 미생성으로 이어졌다. 텍스처\n // 누락은 무시하고 모델 형태만이라도 캡처하도록 placeholder 응답.\n // (.bin 외부 buffer 와 wasm decoder 는 본질적 한계 — 그대로 통과.)\n if (/\\.(png|jpe?g|webp|gif|ktx2|basis)(\\?|$)/i.test(url)) {\n await req.respond({ status: 200, contentType: 'image/png', body: TINY_PNG })\n return\n }\n req.continue()\n })\n\n await page.goto(VIEWER_URL, { waitUntil: 'domcontentloaded' })\n\n await page.waitForFunction('window.__gltfReady === true || window.__gltfError', {\n timeout: TIMEOUT_MS\n })\n\n const err = await page.evaluate('window.__gltfError')\n if (err) {\n logger.warn(`[attachment] gltf thumbnail error: ${err}`)\n return\n }\n\n // puppeteer 신버전은 Uint8Array 를 반환하는데 Uint8Array.toString('base64') 는 encoding 인자를\n // 무시해서 decimal comma string 이 나온다. encoding: 'base64' 를 직접 요청해서 문자열을 받는다.\n const b64 = (await page.screenshot({ type: 'jpeg', quality: 70, encoding: 'base64' })) as string\n return `data:image/jpeg;base64,${b64}`\n } catch (e) {\n logger.warn(`[attachment] gltf thumbnail failed: ${(e as Error).message}`)\n return\n } finally {\n try { await page.close() } catch {}\n try { await pool.release(browser) } catch {}\n }\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 업로드 파일의 mimetype 정규화.
|
|
3
|
+
*
|
|
4
|
+
* Apollo `graphql-upload` 의 FileUpload.mimetype 은 클라이언트가 보낸
|
|
5
|
+
* multipart/form-data 의 Content-Type 을 그대로 받는다. 이 값은 OS / 브라우저의
|
|
6
|
+
* MIME 데이터베이스가 확장자를 인식하느냐에 따라 같은 파일이라도 다르게 들어온다.
|
|
7
|
+
* 대표적으로 GLB:
|
|
8
|
+
* - .glb 가 MIME DB 에 등록된 환경 → 'model/gltf-binary'
|
|
9
|
+
* - 미등록 환경 (Windows 일부 등) → 'application/octet-stream'
|
|
10
|
+
* - zip 해제 / 메일 첨부 거치면서 손실 → 'application/octet-stream'
|
|
11
|
+
*
|
|
12
|
+
* 이 불일치 때문에 썸네일 생성, 검색, category 분류 등 후속 로직이 같은 파일에
|
|
13
|
+
* 대해 다르게 동작했다. 본 모듈은 모호한 mime ('' / 'application/octet-stream')
|
|
14
|
+
* 일 때만 magic byte → 확장자 순으로 재판정하고, 의미 있는 mime 은 그대로 둔다.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 모호한 원본 mimetype 을 컨텐츠/파일명 기반으로 재판정한다.
|
|
18
|
+
*
|
|
19
|
+
* 정규화 규칙:
|
|
20
|
+
* 1. 원본 mime 이 의미 있으면 (예: 'image/png', 'model/gltf-binary') 그대로 보존.
|
|
21
|
+
* 2. 원본 mime 이 모호한 경우 (`''`, `application/octet-stream`,
|
|
22
|
+
* `binary/octet-stream`) magic byte → 확장자 순으로 추론.
|
|
23
|
+
* 3. 추론도 실패하면 원본 그대로 반환 (정보 손실 없음).
|
|
24
|
+
*
|
|
25
|
+
* 이 함수는 동일 파일이 환경 차이로 다른 mime 으로 들어와도 일관된 mime 으로
|
|
26
|
+
* 저장/처리되도록 보장한다 — 썸네일 생성, 카테고리 분류, backfill 쿼리 등
|
|
27
|
+
* mime 을 키로 하는 후속 로직 모두에 영향.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeMimetype(originalMime: string | undefined, filename: string | undefined, contents: Buffer | undefined | null): string;
|
|
30
|
+
/**
|
|
31
|
+
* 외부에서 (예: 백필 쿼리) 모호한 mime 으로 저장된 첨부파일의 실제 포맷을 추정.
|
|
32
|
+
* normalizeMimetype 와 동일한 매직바이트/확장자 결과를 반환하지만, 의미 있는
|
|
33
|
+
* 원본 mime 보존 정책을 거치지 않는다 — 이미 저장된 데이터의 실제 포맷이
|
|
34
|
+
* 무엇인지를 알아내야 하는 backfill 시나리오 전용.
|
|
35
|
+
*/
|
|
36
|
+
export declare function detectMimetypeFromContent(filename: string | undefined, contents: Buffer | undefined | null): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* mimetype 으로부터 attachment.category 결정.
|
|
39
|
+
*
|
|
40
|
+
* 기본 규칙은 mimetype 의 main type ('image/png' → 'image'). 단, 3D 모델
|
|
41
|
+
* (model/*) 은 시각 자산이라 사용자 인지 측면에서 'image' 카테고리로 묶는다.
|
|
42
|
+
* 별도의 'model' 카테고리를 두지 않은 이유는 기존 attachment picker UI 들이
|
|
43
|
+
* 'image' 카테고리를 기준으로 시각 자산을 필터링하기 때문 — picker 마다 옵션
|
|
44
|
+
* 추가 작업 부담을 피하기 위한 실용적 선택. 향후 3D 모델 전용 picker 가
|
|
45
|
+
* 필요해지면 'model' 분리를 재검토.
|
|
46
|
+
*/
|
|
47
|
+
export declare function categoryFromMimetype(mimetype: string | undefined): string;
|