@things-factory/attachment-base 8.0.0-beta.0 → 8.0.0-beta.2
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 +5 -5
- package/server/attachment-const.ts +0 -5
- package/server/awb-storage-s3.ts +0 -44
- package/server/index.ts +0 -24
- package/server/nfc-normalize.ts +0 -25
- package/server/routes.ts +0 -35
- package/server/service/attachment/attachment-mutation.ts +0 -343
- package/server/service/attachment/attachment-query.ts +0 -78
- package/server/service/attachment/attachment-types.ts +0 -76
- package/server/service/attachment/attachment.ts +0 -129
- package/server/service/attachment/index.ts +0 -6
- package/server/service/index.ts +0 -19
- package/server/storage-azure-blob.ts +0 -111
- package/server/storage-database.ts +0 -80
- package/server/storage-file.ts +0 -80
- package/server/storage-s3.ts +0 -141
- package/server/util/index.ts +0 -1
- package/server/util/upload-awb.ts +0 -11
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@things-factory/attachment-base",
|
3
|
-
"version": "8.0.0-beta.
|
3
|
+
"version": "8.0.0-beta.2",
|
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.
|
33
|
-
"@things-factory/env": "^8.0.0-beta.
|
34
|
-
"@things-factory/shell": "^8.0.0-beta.
|
32
|
+
"@things-factory/auth-base": "^8.0.0-beta.2",
|
33
|
+
"@things-factory/env": "^8.0.0-beta.2",
|
34
|
+
"@things-factory/shell": "^8.0.0-beta.2",
|
35
35
|
"mime": "^3.0.0",
|
36
36
|
"multer": "^1.4.5-lts.1"
|
37
37
|
},
|
38
|
-
"gitHead": "
|
38
|
+
"gitHead": "f03431a09435511b2595515658f9cb8f78ba4ebb"
|
39
39
|
}
|
package/server/awb-storage-s3.ts
DELETED
@@ -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
|
-
})
|
package/server/nfc-normalize.ts
DELETED
@@ -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
|
-
}
|
package/server/service/index.ts
DELETED
@@ -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
|
-
}
|
package/server/storage-file.ts
DELETED
@@ -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
|
-
}
|
package/server/storage-s3.ts
DELETED
@@ -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
|
-
}
|
package/server/util/index.ts
DELETED
@@ -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
|
-
}
|