@things-factory/attachment-base 8.0.0-beta.1 → 8.0.0-beta.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/attachment-base",
3
- "version": "8.0.0-beta.1",
3
+ "version": "8.0.0-beta.4",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "client/index.js",
6
6
  "things-factory": true,
@@ -29,11 +29,11 @@
29
29
  "@aws-sdk/s3-presigned-post": "^3.46.0",
30
30
  "@azure/storage-blob": "^12.18.0",
31
31
  "@koa/multer": "^3.0.0",
32
- "@things-factory/auth-base": "^8.0.0-beta.1",
33
- "@things-factory/env": "^8.0.0-beta.1",
34
- "@things-factory/shell": "^8.0.0-beta.1",
32
+ "@things-factory/auth-base": "^8.0.0-beta.4",
33
+ "@things-factory/env": "^8.0.0-beta.4",
34
+ "@things-factory/shell": "^8.0.0-beta.4",
35
35
  "mime": "^3.0.0",
36
36
  "multer": "^1.4.5-lts.1"
37
37
  },
38
- "gitHead": "36c494e587640c1490318ef7b95adab02606e0c2"
38
+ "gitHead": "d83d12ed4ba07177dff1dac26e37be347d156b43"
39
39
  }
@@ -1,5 +0,0 @@
1
- import { config } from '@things-factory/env'
2
-
3
- export var STORAGE: any = config.get('storage')
4
- export var AWBSTORAGE: any = config.get('awbFileStorage')
5
- export const ATTACHMENT_PATH: string = config.get('attachmentPath', 'attachment')
@@ -1,44 +0,0 @@
1
- import { S3Client } from '@aws-sdk/client-s3'
2
- import { Upload } from '@aws-sdk/lib-storage'
3
- import { logger } from '@things-factory/env'
4
-
5
- import { AWBSTORAGE } from './attachment-const'
6
-
7
- if (AWBSTORAGE && AWBSTORAGE.type == 's3') {
8
- const client = new S3Client({
9
- credentials: {
10
- accessKeyId: AWBSTORAGE.accessKeyId,
11
- secretAccessKey: AWBSTORAGE.secretAccessKey
12
- },
13
- region: AWBSTORAGE.region
14
- })
15
-
16
- /* upload file */
17
- AWBSTORAGE.uploadFile = async ({ stream, filename }) => {
18
- const upload = new Upload({
19
- client,
20
- params: {
21
- Bucket: AWBSTORAGE.bucketName,
22
- Key: `${filename}.pdf`,
23
- Body: stream,
24
- ContentType: 'application/pdf'
25
- }
26
- })
27
-
28
- let result
29
- let url
30
- try {
31
- result = (await upload.done()) as any
32
- url = `https://${AWBSTORAGE.bucketName}.s3.${AWBSTORAGE.region}.amazonaws.com/${filename}.pdf`
33
- } catch (e) {
34
- console.log(e)
35
- }
36
-
37
- return {
38
- result,
39
- url
40
- }
41
- }
42
-
43
- logger.info('operato-awb: S3 Bucket Storage is Ready.')
44
- }
package/server/index.ts DELETED
@@ -1,24 +0,0 @@
1
- export * from './attachment-const'
2
- export * from './service'
3
- export {
4
- createAttachment,
5
- createAttachments,
6
- deleteAttachment,
7
- deleteAttachmentsByRef,
8
- multipleUpload,
9
- singleUpload
10
- } from './service/attachment/attachment-mutation'
11
-
12
- import './routes'
13
-
14
- export * from './attachment-const'
15
- export * from './util'
16
-
17
- import { normalizeNamesToNFC } from './nfc-normalize'
18
-
19
- process.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {
20
- /* 이 코드는 기존 첨부파일의 NFC 정규화를 강제로 실행하기 위해서 임시로 작성되었다. */
21
- normalizeNamesToNFC()
22
- .then(() => console.log('Normalization completed.'))
23
- .catch(err => console.error('Error during normalization:', err))
24
- })
@@ -1,25 +0,0 @@
1
- import { getRepository } from '@things-factory/shell'
2
- import { Attachment } from './service/attachment/attachment'
3
-
4
- export async function normalizeNamesToNFC() {
5
- const attachmentRepository = getRepository(Attachment)
6
-
7
- // 모든 Attachment 항목을 조회
8
- const attachments = await attachmentRepository.find()
9
-
10
- // NFD로 저장된 name을 NFC로 변환하여 업데이트
11
- for (const attachment of attachments) {
12
- if (attachment.name) {
13
- const normalizedName = attachment.name.normalize('NFC')
14
-
15
- // name이 NFD로 저장된 경우만 업데이트
16
- if (attachment.name !== normalizedName) {
17
- attachment.name = normalizedName
18
- await attachmentRepository.save(attachment)
19
- console.log(`Updated name for attachment ID ${attachment.id}: ${normalizedName}`)
20
- }
21
- }
22
- }
23
-
24
- console.log('All names have been normalized to NFC.')
25
- }
package/server/routes.ts DELETED
@@ -1,35 +0,0 @@
1
- import './storage-file'
2
- import './storage-s3'
3
- import './storage-database'
4
- import './storage-azure-blob'
5
-
6
- import { ATTACHMENT_PATH, STORAGE } from './attachment-const'
7
-
8
- const multer = require('@koa/multer')
9
- const upload = multer() // note you can pass `multer` options here
10
-
11
- const { Readable } = require('stream')
12
-
13
- // process.on('bootstrap-module-domain-private-route' as any, (app, routes) => {
14
- process.on('bootstrap-module-global-public-route' as any, (app, routes) => {
15
- // TODO make this secure
16
- routes.get(`/${ATTACHMENT_PATH}/:attachment`, async (context, next) => {
17
- context.set('Cache-Control', 'public, max-age=31536000') // 캐시 기간 설정 : 1년
18
- context.set('Expires', new Date(Date.now() + 31536000000).toUTCString()) // 캐시 만료일 설정
19
-
20
- await STORAGE.sendFile(context, context.params.attachment, next)
21
- })
22
-
23
- routes.post(`/${ATTACHMENT_PATH}`, upload.any(), async (context, next) => {
24
- const files = context.files
25
-
26
- const result: { id: string; path: string; size: number }[] = await Promise.all(
27
- files.map(file => STORAGE.uploadFile({ file, context }))
28
- )
29
-
30
- context.status = 200
31
- // Support < IE 10 browser
32
- context.set('Content-Type', 'text/html;charset=UTF-8')
33
- context.body = JSON.stringify(result)
34
- })
35
- })
@@ -1,343 +0,0 @@
1
- import { FileUpload } from 'graphql-upload/GraphQLUpload.js'
2
- import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
3
- import promisesAll from 'promises-all'
4
-
5
- import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'
6
- import { In } from 'typeorm'
7
-
8
- import { config, logger } from '@things-factory/env'
9
- import { getRepository } from '@things-factory/shell'
10
-
11
- import { STORAGE } from '../../attachment-const'
12
- import { Attachment } from './attachment'
13
- import { AttachmentPatch, NewAttachment, UploadURL } from './attachment-types'
14
-
15
- const allowedMimeTypes = config.get('fileUpload/mimeTypes', [])
16
-
17
- @Resolver(Attachment)
18
- export class AttachmentMutation {
19
- @Directive('@transaction')
20
- @Mutation(returns => Attachment)
21
- async createAttachment(
22
- @Arg('attachment', type => NewAttachment) attachment: NewAttachment,
23
- @Ctx() context: ResolverContext
24
- ): Promise<Attachment> {
25
- return await createAttachment(null, { attachment }, context)
26
- }
27
-
28
- @Directive('@transaction')
29
- @Mutation(returns => [Attachment])
30
- async createAttachments(
31
- @Arg('attachments', type => [NewAttachment]) attachments: NewAttachment[],
32
- @Ctx() context: ResolverContext
33
- ): Promise<Attachment[]> {
34
- return await createAttachments(null, { attachments }, context)
35
- }
36
-
37
- @Directive('@transaction')
38
- @Mutation(returns => Attachment)
39
- async updateAttachment(
40
- @Arg('id') id: string,
41
- @Arg('patch', type => AttachmentPatch) patch: AttachmentPatch,
42
- @Ctx() context: ResolverContext
43
- ): Promise<Attachment> {
44
- const attachment = await getRepository(Attachment).findOne({
45
- where: {
46
- domain: { id: context.state.domain.id },
47
- id
48
- }
49
- })
50
-
51
- return await getRepository(Attachment).save({
52
- ...attachment,
53
- ...patch,
54
- updater: context.state.user
55
- })
56
- }
57
-
58
- @Directive('@transaction')
59
- @Mutation(returns => Boolean)
60
- async deleteAttachment(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
61
- return await deleteAttachment(null, { id }, context)
62
- }
63
-
64
- @Directive('@transaction')
65
- @Mutation(returns => Boolean)
66
- async deleteAttachmentsByRef(
67
- @Arg('refBys', type => [String]) refBys: string[],
68
- @Arg('refType', type => String, { nullable: true }) refType: string,
69
- @Ctx() context: ResolverContext
70
- ): Promise<boolean> {
71
- return await deleteAttachmentsByRef(null, { refBys, refType }, context)
72
- }
73
-
74
- @Directive('@transaction')
75
- @Mutation(returns => Attachment)
76
- async singleUpload(
77
- @Arg('file', type => GraphQLUpload) file: FileUpload,
78
- @Ctx() context: ResolverContext
79
- ): Promise<Attachment> {
80
- return await singleUpload(null, { file }, context)
81
- }
82
-
83
- @Directive('@transaction')
84
- @Mutation(returns => [Attachment])
85
- async multipleUpload(
86
- @Arg('files', type => [GraphQLUpload]) files: FileUpload[],
87
- @Ctx() context: ResolverContext
88
- ): Promise<Attachment[]> {
89
- return await multipleUpload(null, { files }, context)
90
- }
91
-
92
- @Mutation(returns => UploadURL)
93
- async generateUploadURL(
94
- @Arg('type', type => String) type: string,
95
- @Ctx() context: ResolverContext
96
- ): Promise<{ url: string; fields: { [key: string]: string } }> {
97
- return await generateUploadURL(null, { type }, context)
98
- }
99
-
100
- @Directive('@transaction')
101
- @Mutation(returns => [Attachment], { description: 'To import some Attachments' })
102
- async importAttachments(
103
- @Arg('file', () => GraphQLUpload) file: FileUpload,
104
- @Ctx() context: ResolverContext
105
- ): Promise<Attachment[]> {
106
- return await importAttachments(file, context)
107
- }
108
- }
109
-
110
- export async function createAttachment(_: any, { attachment }, context: ResolverContext): Promise<Attachment> {
111
- const { file, category, refType = '', refBy, description } = attachment
112
- const { mimetype } = (await file) as FileUpload
113
-
114
- if (allowedMimeTypes instanceof Array && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes('*/*')) {
115
- const isAllowed = allowedMimeTypes.some(type => {
116
- const [typeMain, typeSub] = type.split('/')
117
- const [mimeMain, mimeSub] = mimetype.split('/')
118
- return (
119
- (typeMain === mimeMain && (typeSub === '*' || typeSub === mimeSub)) || (typeMain === '*' && typeSub === '*')
120
- )
121
- })
122
-
123
- if (!isAllowed) {
124
- throw Error(context.t(`error.not allowed file type for upload`, { mimetype }))
125
- }
126
- }
127
-
128
- const { id, path, size, filename, encoding, contents } = await STORAGE.uploadFile({ file, context })
129
- const { domain, user, tx } = context.state
130
-
131
- const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)
132
-
133
- return await repository.save({
134
- domain,
135
- creator: user,
136
- updater: user,
137
- id,
138
- description,
139
- name: filename,
140
- mimetype,
141
- encoding,
142
- refType,
143
- refBy,
144
- category: category || mimetype.split('/').shift(),
145
- size: size as any,
146
- path,
147
- contents
148
- })
149
- }
150
-
151
- export async function createAttachments(_: any, { attachments }, context: ResolverContext): Promise<Attachment[]> {
152
- const { resolve, reject } = await promisesAll.all(
153
- attachments.map(attachment => createAttachment(_, { attachment }, context))
154
- )
155
-
156
- if (reject.length) {
157
- reject.forEach(({ name, message }) => logger.error(`${name}: ${message}`))
158
-
159
- return reject
160
- }
161
-
162
- return resolve
163
- }
164
-
165
- export async function deleteAttachment(_: any, { id }, context: ResolverContext): Promise<boolean> {
166
- const { domain, tx } = context.state
167
-
168
- const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)
169
- const attachment = await repository.findOne({
170
- where: { domain: { id: domain.id }, id }
171
- })
172
-
173
- if (attachment) {
174
- await repository.delete({ id: attachment.id })
175
- await STORAGE.deleteFile(attachment.path)
176
- return true
177
- } else {
178
- return false
179
- }
180
- }
181
-
182
- interface DeleteAttachmentsObject {
183
- refBys: Array<string>
184
- refType?: string
185
- }
186
- export async function deleteAttachmentsByRef(
187
- _: any,
188
- { refBys, refType = null }: DeleteAttachmentsObject,
189
- context: ResolverContext
190
- ): Promise<boolean> {
191
- const { domain, tx } = context.state
192
- const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)
193
- const inquryWhereClause: any = { domain: { id: domain.id }, refBy: In(refBys) }
194
- const deleteWhereClause: any = { refBy: In(refBys) }
195
-
196
- // refType이 존재하면 where 절에 추가
197
- if (refType) {
198
- inquryWhereClause.refType = refType;
199
- deleteWhereClause.refType = refType;
200
- }
201
-
202
- const attachments = await repository.find({
203
- where: inquryWhereClause
204
- })
205
-
206
- //remove attachment from repo
207
- await repository.delete(deleteWhereClause)
208
-
209
- //remove files from attachments folder
210
- if (attachments.length) {
211
- await Promise.all(
212
- attachments.map(async attachment => {
213
- await STORAGE.deleteFile(attachment.path)
214
- })
215
- )
216
-
217
- return true
218
- } else {
219
- return false
220
- }
221
- }
222
-
223
- export async function generateUploadURL(
224
- _: any,
225
- { type },
226
- context: ResolverContext
227
- ): Promise<{ url: string; fields: { [key: string]: string } }> {
228
- return await STORAGE.generateUploadURL(type)
229
- }
230
-
231
- export async function singleUpload(_: any, { file }, context: ResolverContext): Promise<Attachment> {
232
- return await createAttachment(null, { attachment: { file } }, context)
233
- }
234
-
235
- export async function multipleUpload(_: any, { files }, context: ResolverContext): Promise<Attachment[]> {
236
- return await createAttachments(null, { attachments: { files } }, context)
237
- }
238
-
239
- async function parseJSONFile(uploadedFile: FileUpload): Promise<any> {
240
- var { createReadStream } = await uploadedFile
241
-
242
- return new Promise((resolve, reject) => {
243
- const chunks: Uint8Array[] = []
244
-
245
- createReadStream()
246
- .on('data', (chunk: Uint8Array) => {
247
- chunks.push(chunk)
248
- })
249
- .on('end', () => {
250
- try {
251
- const fileContents = Buffer.concat(chunks).toString('utf-8')
252
- const jsonData = JSON.parse(fileContents)
253
- resolve(jsonData)
254
- } catch (error) {
255
- reject(error)
256
- }
257
- })
258
- .on('error', (error: Error) => {
259
- reject(error)
260
- })
261
- })
262
- }
263
-
264
- function dataURLToFileUpload(dataURL: string, filename: string, mimeType: string): FileUpload {
265
- const indexOfComma = dataURL.indexOf(',')
266
- if (indexOfComma === -1) {
267
- throw new Error('Invalid Data URL')
268
- }
269
-
270
- const base64Data = dataURL.slice(indexOfComma + 1)
271
- const buffer = Buffer.from(base64Data, 'base64')
272
-
273
- return {
274
- filename,
275
- mimetype: mimeType,
276
- encoding: 'base64',
277
- createReadStream: () => {
278
- const stream = require('stream')
279
- const readable = new stream.Readable()
280
- readable.push(buffer)
281
- readable.push(null)
282
- return readable
283
- }
284
- }
285
- }
286
-
287
- export async function importAttachments(upload: FileUpload, context: ResolverContext): Promise<Attachment[]> {
288
- const { domain, user, notify, tx } = context.state
289
-
290
- const repository = tx.getRepository(Attachment)
291
-
292
- const attachments = []
293
-
294
- const parsed = await parseJSONFile(upload)
295
-
296
- for (const id in parsed) {
297
- var { name, description, category, mimetype, encoding, contents } = parsed[id]
298
- if (!name || !contents || !mimetype) {
299
- throw 'Malformed attachments import file'
300
- }
301
-
302
- var sameIdAttachment = await repository.findOneBy({ id })
303
-
304
- if (sameIdAttachment) {
305
- if (sameIdAttachment.domainId != domain.id) {
306
- throw `Attachment with id/name(${id}/${name}) is already taken in another domain`
307
- }
308
-
309
- // 동일 아이디 첨부파일이 있다면, 스킵한다.
310
- continue
311
- }
312
-
313
- const file = dataURLToFileUpload(contents, name, mimetype)
314
-
315
- var { path, size, contents } = await STORAGE.uploadFile({ id, file, context })
316
-
317
- attachments.push(
318
- await repository.save({
319
- domain,
320
- creator: user,
321
- updater: user,
322
- id,
323
- description,
324
- name,
325
- mimetype,
326
- encoding,
327
- category: category || mimetype.split('/').shift(),
328
- size: size as any,
329
- path,
330
- contents
331
- })
332
- )
333
- }
334
-
335
- notify &&
336
- notify({
337
- mode: 'in-app',
338
- title: `${attachments.length} Attachment(s) are imported`,
339
- body: `${attachments.length} Attachment(s) are imported by ${user.name}`
340
- })
341
-
342
- return attachments
343
- }
@@ -1,78 +0,0 @@
1
- import { In } from 'typeorm'
2
- import { Arg, Args, Ctx, Directive, FieldResolver, Query, Resolver, Root } from 'type-graphql'
3
-
4
- import { User } from '@things-factory/auth-base'
5
- import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
6
-
7
- import { AttachmentList } from '../'
8
- import { Attachment } from './attachment'
9
-
10
- @Resolver(Attachment)
11
- export class AttachmentQuery {
12
- @Directive('@privilege(category: "attachment", privilege: "query", domainOwnerGranted: true)')
13
- @Query(returns => AttachmentList)
14
- async attachments(
15
- @Ctx() context: ResolverContext,
16
- @Args(type => ListParam) params: ListParam
17
- ): Promise<AttachmentList> {
18
- const { domain } = context.state
19
-
20
- const queryBuilder = getQueryBuilderFromListParams({
21
- repository: await getRepository(Attachment),
22
- params,
23
- domain,
24
- alias: 'attachment',
25
- searchables: ['name', 'description', 'tags']
26
- })
27
-
28
- const [items, total] = await queryBuilder.getManyAndCount()
29
-
30
- return { items, total }
31
- }
32
-
33
- @Directive('@privilege(category: "attachment", privilege: "query", domainOwnerGranted: true)')
34
- @Query(returns => Attachment)
35
- async attachment(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Attachment> {
36
- const { domain } = context.state
37
-
38
- return await getRepository(Attachment).findOne({
39
- select: [
40
- 'domain',
41
- 'id',
42
- 'name',
43
- 'path',
44
- 'size',
45
- 'mimetype',
46
- 'encoding',
47
- 'category',
48
- 'updatedAt',
49
- 'updater',
50
- 'createdAt',
51
- 'creator'
52
- ],
53
- where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id },
54
- relations: ['domain', 'creator', 'updater']
55
- })
56
- }
57
-
58
- @FieldResolver(type => Domain)
59
- async domain(@Root() attachment: Attachment) {
60
- return await getRepository(Domain).findOneBy({
61
- id: attachment.domainId
62
- })
63
- }
64
-
65
- @FieldResolver(type => User)
66
- async updater(@Root() attachment: Attachment): Promise<User> {
67
- return await getRepository(User).findOneBy({
68
- id: attachment.updaterId
69
- })
70
- }
71
-
72
- @FieldResolver(type => User)
73
- async creator(@Root() attachment: Attachment): Promise<User> {
74
- return await getRepository(User).findOneBy({
75
- id: attachment.creatorId
76
- })
77
- }
78
- }
@@ -1,76 +0,0 @@
1
- import type { FileUpload } from 'graphql-upload/GraphQLUpload.js'
2
- import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
3
- import { Field, InputType, Int, ObjectType } from 'type-graphql'
4
-
5
- import { ScalarAny, ScalarObject } from '@things-factory/shell'
6
-
7
- import { Attachment } from './attachment'
8
-
9
- @ObjectType()
10
- export class AttachmentList {
11
- @Field(type => [Attachment])
12
- items: Attachment[]
13
-
14
- @Field(type => Int)
15
- total: number
16
- }
17
-
18
- @ObjectType()
19
- export class UploadURL {
20
- @Field(type => String)
21
- url: string
22
-
23
- @Field(type => ScalarAny)
24
- fields: any
25
- }
26
-
27
- @InputType()
28
- export class NewAttachment {
29
- @Field({ nullable: true })
30
- category: string
31
-
32
- @Field(type => GraphQLUpload)
33
- file: FileUpload
34
-
35
- @Field({ nullable: true })
36
- description: string
37
-
38
- @Field({ nullable: true })
39
- refType: string
40
-
41
- @Field({ nullable: true })
42
- refBy: string
43
-
44
- @Field(type => ScalarObject, { nullable: true })
45
- tags?: string[]
46
- }
47
-
48
- @InputType()
49
- export class AttachmentPatch {
50
- @Field({ nullable: true })
51
- name: string
52
-
53
- @Field({ nullable: true })
54
- description: string
55
-
56
- @Field({ nullable: true })
57
- mimetype: string
58
-
59
- @Field({ nullable: true })
60
- encoding: string
61
-
62
- @Field({ nullable: true })
63
- category: string
64
-
65
- @Field(type => GraphQLUpload, { nullable: true })
66
- file: FileUpload
67
-
68
- @Field({ nullable: true })
69
- refType: string
70
-
71
- @Field({ nullable: true })
72
- refBy: string
73
-
74
- @Field(type => ScalarObject, { nullable: true })
75
- tags?: string[]
76
- }
@@ -1,129 +0,0 @@
1
- import { Field, ID, ObjectType } from 'type-graphql'
2
- import {
3
- Column,
4
- CreateDateColumn,
5
- Entity,
6
- Index,
7
- ManyToOne,
8
- PrimaryGeneratedColumn,
9
- RelationId,
10
- UpdateDateColumn
11
- } from 'typeorm'
12
-
13
- import { User } from '@things-factory/auth-base'
14
- import { Domain, ScalarObject } from '@things-factory/shell'
15
-
16
- import { ATTACHMENT_PATH } from '../../attachment-const'
17
- import { config } from '@things-factory/env'
18
-
19
- const ORMCONFIG = config.get('ormconfig', {})
20
- const DATABASE_TYPE = ORMCONFIG.type
21
-
22
- @Entity()
23
- @Index('ix_attachment_0', (attachment: Attachment) => [attachment.domain, attachment.name], { unique: false })
24
- @Index('ix_attachment_1', (attachment: Attachment) => [attachment.domain, attachment.category, attachment.name], {
25
- unique: false
26
- })
27
- @Index('ix_attachment_2', (attachment: Attachment) => [attachment.domain, attachment.refBy], {
28
- unique: false
29
- })
30
- @Index('ix_attachment_3', (attachment: Attachment) => [attachment.domain, attachment.refType, attachment.refBy], {
31
- unique: false
32
- })
33
- @ObjectType()
34
- export class Attachment {
35
- @PrimaryGeneratedColumn('uuid')
36
- @Field(type => ID)
37
- readonly id?: string
38
-
39
- @ManyToOne(type => Domain, { nullable: false })
40
- @Field(type => Domain)
41
- domain?: Domain
42
-
43
- @RelationId((attachment: Attachment) => attachment.domain)
44
- domainId?: string
45
-
46
- @Column()
47
- @Field()
48
- name?: string
49
-
50
- @Column({ nullable: true })
51
- @Field({ nullable: true })
52
- description?: string
53
-
54
- @Column()
55
- @Field()
56
- mimetype?: string
57
-
58
- @Column()
59
- @Field()
60
- encoding?: string
61
-
62
- @Column({ nullable: true })
63
- @Field({ nullable: true })
64
- category?: string
65
-
66
- @Column({ nullable: true, default: '' })
67
- @Field({ nullable: true })
68
- refType?: string = ''
69
-
70
- @Column({ nullable: true })
71
- @Field({ nullable: true })
72
- refBy?: string
73
-
74
- @Column()
75
- @Field()
76
- path?: string
77
-
78
- @Column({
79
- nullable: true,
80
- type: DATABASE_TYPE == 'mssql' ? 'bigint' : undefined
81
- })
82
- @Field()
83
- size?: string
84
-
85
- @Column({
86
- nullable: true,
87
- type:
88
- DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
89
- ? 'longblob'
90
- : DATABASE_TYPE == 'postgres'
91
- ? 'bytea'
92
- : DATABASE_TYPE == 'mssql'
93
- ? 'varbinary'
94
- : 'blob',
95
- length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
96
- })
97
- contents?: Buffer
98
-
99
- @Column('simple-json', { nullable: true, default: null })
100
- @Field(type => ScalarObject, { nullable: true })
101
- tags?: string[]
102
-
103
- @CreateDateColumn()
104
- @Field()
105
- createdAt?: Date
106
-
107
- @UpdateDateColumn()
108
- @Field()
109
- updatedAt?: Date
110
-
111
- @ManyToOne(type => User)
112
- @Field(type => User, { nullable: true })
113
- creator?: User
114
-
115
- @RelationId((attachment: Attachment) => attachment.creator)
116
- creatorId?: string
117
-
118
- @ManyToOne(type => User)
119
- @Field(type => User, { nullable: true })
120
- updater?: User
121
-
122
- @RelationId((attachment: Attachment) => attachment.updater)
123
- updaterId?: string
124
-
125
- @Field()
126
- get fullpath(): string {
127
- return `/${ATTACHMENT_PATH}/${this.path}`
128
- }
129
- }
@@ -1,6 +0,0 @@
1
- import { Attachment } from './attachment'
2
- import { AttachmentMutation } from './attachment-mutation'
3
- import { AttachmentQuery } from './attachment-query'
4
-
5
- export const entities = [Attachment]
6
- export const resolvers = [AttachmentQuery, AttachmentMutation]
@@ -1,19 +0,0 @@
1
- /* IMPORT ENTITIES AND RESOLVERS */
2
- import { entities as AttachmentEntity, resolvers as AttachmentResolvers } from './attachment'
3
-
4
- /* EXPORT ENTITY TYPES */
5
- export * from './attachment/attachment'
6
- /* EXPORT TYPES */
7
- export * from './attachment/attachment-types'
8
-
9
- export const entities = [
10
- /* ENTITIES */
11
- ...AttachmentEntity
12
- ]
13
-
14
- export const schema = {
15
- resolverClasses: [
16
- /* RESOLVER CLASSES */
17
- ...AttachmentResolvers
18
- ]
19
- }
@@ -1,111 +0,0 @@
1
- import { BlobServiceClient } from '@azure/storage-blob'
2
- import { logger } from '@things-factory/env'
3
-
4
- import { STORAGE } from './attachment-const'
5
-
6
- const crypto = require('crypto')
7
- const mime = require('mime')
8
-
9
- if (STORAGE && STORAGE.type == 'azureblob') {
10
- const blobServiceClient = BlobServiceClient.fromConnectionString(STORAGE.connectionString)
11
-
12
- /* upload file */
13
- STORAGE.uploadFile = async ({ id, file }) => {
14
- const { createReadStream, filename, mimetype, encoding } = await file
15
-
16
- const containerClient = blobServiceClient.getContainerClient(STORAGE.containerName)
17
- id = id || crypto.randomUUID()
18
- const ext = filename.split('.').pop()
19
- const key = ext ? `${id}.${ext}` : id
20
-
21
- const blockBlobClient = containerClient.getBlockBlobClient(key)
22
- const stream = createReadStream()
23
- const buffer = await streamToBuffer(stream)
24
-
25
- await blockBlobClient.upload(buffer, buffer.length, {
26
- blobHTTPHeaders: {
27
- blobContentType: mimetype
28
- }
29
- })
30
-
31
- // await blockBlobClient.uploadStream(stream, undefined, undefined, {
32
- // blobHTTPHeaders: {
33
- // blobContentType: mimetype
34
- // }
35
- // })
36
-
37
- const url = `${STORAGE.url}/${STORAGE.containerName}/${key}`
38
- return {
39
- id,
40
- path: key,
41
- filename,
42
- size: buffer.length,
43
- mimetype,
44
- encoding
45
- }
46
- }
47
-
48
- STORAGE.deleteFile = async (path: string) => {
49
- const containerClient = blobServiceClient.getContainerClient(STORAGE.containerName)
50
- const blockBlobClient = containerClient.getBlockBlobClient(path)
51
- await blockBlobClient.deleteIfExists()
52
- }
53
-
54
- /* TODO Streaming to Streaming 으로 구현하라. */
55
- STORAGE.sendFile = async (context, attachment, next) => {
56
- const containerClient = blobServiceClient.getContainerClient(STORAGE.containerName)
57
- const blockBlobClient = containerClient.getBlockBlobClient(attachment)
58
-
59
- const result = await blockBlobClient.getProperties()
60
- const response = await blockBlobClient.download(0)
61
- const body = response.readableStreamBody
62
-
63
- context.set({
64
- 'Content-Length': result.contentLength,
65
- 'Content-Type': mime.getType(attachment),
66
- 'Last-Modified': result.lastModified.toUTCString(),
67
- ETag: result.etag,
68
- 'Cache-Control': 'public, max-age=31556926'
69
- })
70
-
71
- context.body = body
72
- }
73
-
74
- STORAGE.readFile = async (attachment: string, encoding: string) => {
75
- const containerClient = blobServiceClient.getContainerClient(STORAGE.containerName)
76
- const blockBlobClient = containerClient.getBlockBlobClient(attachment)
77
-
78
- const response = await blockBlobClient.download(0)
79
- const body = response.readableStreamBody
80
-
81
- const buffer = Buffer.from(await streamToBuffer(body))
82
-
83
- switch (encoding) {
84
- case 'base64':
85
- return buffer.toString('base64')
86
- default:
87
- return buffer
88
- }
89
- }
90
-
91
- STORAGE.generateUploadURL = async (type: string): Promise<{ url: string; fields: { [key: string]: string } }> => {
92
- const expiresInMinutes = 1
93
- const id = crypto.randomUUID()
94
-
95
- return {
96
- url: `${STORAGE.url}/${STORAGE.containerName}/${id}`,
97
- fields: {}
98
- }
99
- }
100
-
101
- logger.info('Azure Blob Storage is Ready.')
102
- }
103
-
104
- async function streamToBuffer(stream): Promise<Buffer> {
105
- return new Promise<Buffer>((resolve, reject) => {
106
- const chunks = []
107
- stream.on('data', chunk => chunks.push(chunk))
108
- stream.on('end', () => resolve(Buffer.concat(chunks)))
109
- stream.on('error', reject)
110
- })
111
- }
@@ -1,80 +0,0 @@
1
- import contentDisposition from 'content-disposition'
2
-
3
- import { logger } from '@things-factory/env'
4
- import { getRepository } from '@things-factory/shell'
5
-
6
- import { Attachment } from './service/attachment/attachment'
7
- import { ATTACHMENT_PATH, STORAGE } from './attachment-const'
8
-
9
- const crypto = require('crypto')
10
-
11
- if (STORAGE && STORAGE.type == 'database') {
12
- STORAGE.uploadFile = async ({ id, file, context }) => {
13
- var { createReadStream, filename, mimetype, encoding } = await file
14
- filename = Buffer.from(filename, 'latin1')
15
- .toString('utf-8')
16
- .normalize('NFC') /* Because busboy uses latin1 encoding */
17
-
18
- const stream = createReadStream()
19
-
20
- const chunks: Buffer[] = []
21
- for await (const chunk of stream) {
22
- if (chunk instanceof Buffer) {
23
- chunks.push(chunk)
24
- }
25
- }
26
-
27
- id = id || crypto.randomUUID()
28
- const ext = filename.split('.').pop()
29
- const path = ext ? `${id}.${ext}` : id
30
-
31
- const contents = Buffer.concat(chunks)
32
-
33
- return {
34
- id,
35
- filename,
36
- mimetype,
37
- encoding,
38
- contents,
39
- path,
40
- size: contents.length
41
- }
42
- }
43
-
44
- STORAGE.deleteFile = async path => {}
45
-
46
- STORAGE.sendFile = async (context, attachment, next) => {
47
- const id = attachment.split('.')[0]
48
-
49
- const entity = await getRepository(Attachment).findOne({
50
- select: ['name', 'contents'],
51
- where: { id }
52
- })
53
-
54
- context.set('Content-Disposition', contentDisposition(entity.name))
55
- context.body = entity.contents
56
- context.type = entity.mimetype
57
- }
58
-
59
- STORAGE.readFile = async (attachment, encoding) => {
60
- const id = attachment.split('.')[0]
61
-
62
- const entity = await getRepository(Attachment).findOne({
63
- select: ['name', 'contents'],
64
- where: { id }
65
- })
66
-
67
- return await entity.contents
68
- }
69
-
70
- STORAGE.generateUploadURL = async (type: string): Promise<{ url: string; fields: { [key: string]: string } }> => {
71
- const id = crypto.randomUUID()
72
-
73
- return await {
74
- url: `/${ATTACHMENT_PATH}`,
75
- fields: {}
76
- }
77
- }
78
-
79
- logger.info('File Storage is Ready.')
80
- }
@@ -1,80 +0,0 @@
1
- import * as fs from 'fs'
2
- import * as mkdirp from 'mkdirp'
3
- import { resolve } from 'path'
4
-
5
- import { config, logger } from '@things-factory/env'
6
-
7
- import { ATTACHMENT_PATH, STORAGE } from './attachment-const'
8
-
9
- const crypto = require('crypto')
10
- const send = require('koa-send')
11
-
12
- if (STORAGE && STORAGE.type == 'file') {
13
- const uploadDir = config.getPath(null, STORAGE.base || 'attachments')
14
-
15
- STORAGE.uploadFile = async ({ id, file }) => {
16
- var { createReadStream, filename, mimetype, encoding } = await file
17
- filename = Buffer.from(filename, 'latin1')
18
- .toString('utf-8')
19
- .normalize('NFC') /* Because busboy uses latin1 encoding */
20
-
21
- const stream = createReadStream()
22
-
23
- mkdirp.sync(uploadDir)
24
-
25
- id = id || crypto.randomUUID()
26
- const ext = filename.split('.').pop()
27
- const path = ext ? resolve(uploadDir, `${id}.${ext}`) : resolve(uploadDir, id)
28
- const relativePath = path.split('\\').pop().split('/').pop()
29
- var size: number = 0
30
-
31
- return new Promise<{
32
- id: string
33
- filename: string
34
- path: string
35
- size: number
36
- mimetype: string
37
- encoding: string
38
- }>((resolve, reject) =>
39
- stream
40
- .on('error', error => {
41
- if (stream.truncated)
42
- // Delete the truncated file
43
- fs.unlinkSync(path)
44
- reject(error)
45
- })
46
- .on('data', chunk => {
47
- size += chunk.length
48
- })
49
- .pipe(fs.createWriteStream(path))
50
- .on('finish', () => resolve({ id, filename, path: relativePath, size, mimetype, encoding }))
51
- )
52
- }
53
-
54
- STORAGE.deleteFile = async path => {
55
- const fullpath = resolve(uploadDir, path)
56
-
57
- await fs.unlink(fullpath, logger.error)
58
- }
59
-
60
- STORAGE.sendFile = async (context, attachment, next) => {
61
- await send(context, attachment, { root: uploadDir })
62
- }
63
-
64
- STORAGE.readFile = (attachment, encoding) => {
65
- const fullpath = resolve(uploadDir, attachment)
66
-
67
- return fs.readFileSync(fullpath, encoding)
68
- }
69
-
70
- STORAGE.generateUploadURL = async (type: string): Promise<{ url: string; fields: { [key: string]: string } }> => {
71
- const id = crypto.randomUUID()
72
-
73
- return await {
74
- url: `/${ATTACHMENT_PATH}`,
75
- fields: {}
76
- }
77
- }
78
-
79
- logger.info('File Storage is Ready.')
80
- }
@@ -1,141 +0,0 @@
1
- import type { Readable } from 'stream'
2
-
3
- import {
4
- DeleteObjectCommand,
5
- GetObjectCommand,
6
- GetObjectCommandInput,
7
- HeadObjectCommand,
8
- S3Client
9
- } from '@aws-sdk/client-s3'
10
- import { Upload } from '@aws-sdk/lib-storage'
11
- import { createPresignedPost } from '@aws-sdk/s3-presigned-post'
12
- import { logger } from '@things-factory/env'
13
-
14
- import { STORAGE } from './attachment-const'
15
-
16
- const crypto = require('crypto')
17
- const mime = require('mime')
18
-
19
- if (STORAGE && STORAGE.type == 's3') {
20
- const client = new S3Client({
21
- credentials: {
22
- accessKeyId: STORAGE.accessKeyId,
23
- secretAccessKey: STORAGE.secretAccessKey
24
- },
25
- region: STORAGE.region
26
- })
27
-
28
- const streamToBuffer = (stream: Readable) =>
29
- new Promise<Buffer>((resolve, reject) => {
30
- const chunks: Buffer[] = []
31
- stream.on('data', chunk => chunks.push(chunk))
32
- stream.once('end', () => resolve(Buffer.concat(chunks)))
33
- stream.once('error', reject)
34
- })
35
-
36
- /* upload file */
37
- STORAGE.uploadFile = async ({ id, file }) => {
38
- var { createReadStream, filename, mimetype, encoding } = await file
39
- filename = Buffer.from(filename, 'latin1')
40
- .toString('utf-8')
41
- .normalize('NFC') /* Because busboy uses latin1 encoding */
42
-
43
- const stream = createReadStream()
44
- id = id || crypto.randomUUID()
45
- const ext = filename.split('.').pop()
46
- const key = ext ? `${id}.${ext}` : id
47
-
48
- const upload = new Upload({
49
- client,
50
- params: {
51
- Bucket: STORAGE.bucketName,
52
- Key: key,
53
- Body: stream
54
- }
55
- })
56
-
57
- await upload.done()
58
-
59
- const headObjectCommand = new HeadObjectCommand({
60
- Bucket: STORAGE.bucketName,
61
- Key: key
62
- })
63
-
64
- const { ContentLength } = await client.send(headObjectCommand)
65
-
66
- return {
67
- id,
68
- path: key,
69
- filename,
70
- size: ContentLength,
71
- mimetype,
72
- encoding
73
- }
74
- }
75
-
76
- STORAGE.deleteFile = async (path: string) => {
77
- const command = new DeleteObjectCommand({
78
- Bucket: STORAGE.bucketName,
79
- Key: path
80
- })
81
-
82
- return await client.send(command)
83
- }
84
-
85
- /* TODO Streaming to Streaming 으로 구현하라. */
86
- STORAGE.sendFile = async (context, attachment, next) => {
87
- const result = await client.send(
88
- new GetObjectCommand({
89
- Bucket: STORAGE.bucketName,
90
- Key: attachment
91
- } as GetObjectCommandInput)
92
- )
93
-
94
- context.set({
95
- 'Content-Length': result.ContentLength,
96
- 'Content-Type': mime.getType(attachment),
97
- 'Last-Modified': result.LastModified.toUTCString(),
98
- ETag: result.ETag,
99
- 'Cache-Control': 'public, max-age=31556926'
100
- })
101
-
102
- context.body = result.Body
103
- }
104
-
105
- STORAGE.readFile = async (attachment: string, encoding: string) => {
106
- /*
107
- * refered to
108
- * https://transang.me/modern-fetch-and-how-to-get-buffer-output-from-aws-sdk-v3-getobjectcommand/#the-body-type
109
- */
110
- const result = await client.send(
111
- new GetObjectCommand({
112
- Bucket: STORAGE.bucketName,
113
- Key: attachment
114
- } as GetObjectCommandInput)
115
- )
116
-
117
- var body = result.Body as Readable
118
- var buffer = await streamToBuffer(body)
119
-
120
- switch (encoding) {
121
- case 'base64':
122
- return buffer.toString('base64')
123
- default:
124
- return await buffer
125
- }
126
- }
127
-
128
- STORAGE.generateUploadURL = async (type: string): Promise<{ url: string; fields: { [key: string]: string } }> => {
129
- const expiresInMinutes = 1
130
- const id = crypto.randomUUID()
131
-
132
- return await createPresignedPost(client, {
133
- Bucket: STORAGE.bucketName,
134
- Key: id,
135
- Expires: expiresInMinutes * 60,
136
- Conditions: [['eq', '$Content-Type', type]]
137
- })
138
- }
139
-
140
- logger.info('S3 Bucket Storage is Ready.')
141
- }
@@ -1 +0,0 @@
1
- export * from './upload-awb'
@@ -1,11 +0,0 @@
1
- import '../awb-storage-s3'
2
-
3
- import { AWBSTORAGE } from '../attachment-const'
4
-
5
- export async function uploadAwb(param: any) {
6
- const { content, title } = param
7
-
8
- let result = await AWBSTORAGE.uploadFile({ stream: content, filename: title })
9
-
10
- return result
11
- }