@things-factory/attachment-base 8.0.0-beta.9 → 8.0.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.
@@ -0,0 +1,129 @@
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
+ }
@@ -0,0 +1,6 @@
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]
@@ -0,0 +1,19 @@
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
+ }
@@ -0,0 +1,111 @@
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
+ }
@@ -0,0 +1,78 @@
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').toString('utf-8') /* Because busboy uses latin1 encoding */
15
+
16
+ const stream = createReadStream()
17
+
18
+ const chunks: Buffer[] = []
19
+ for await (const chunk of stream) {
20
+ if (chunk instanceof Buffer) {
21
+ chunks.push(chunk)
22
+ }
23
+ }
24
+
25
+ id = id || crypto.randomUUID()
26
+ const ext = filename.split('.').pop()
27
+ const path = ext ? `${id}.${ext}` : id
28
+
29
+ const contents = Buffer.concat(chunks)
30
+
31
+ return {
32
+ id,
33
+ filename,
34
+ mimetype,
35
+ encoding,
36
+ contents,
37
+ path,
38
+ size: contents.length
39
+ }
40
+ }
41
+
42
+ STORAGE.deleteFile = async path => {}
43
+
44
+ STORAGE.sendFile = async (context, attachment, next) => {
45
+ const id = attachment.split('.')[0]
46
+
47
+ const entity = await getRepository(Attachment).findOne({
48
+ select: ['name', 'contents'],
49
+ where: { id }
50
+ })
51
+
52
+ context.set('Content-Disposition', contentDisposition(entity.name))
53
+ context.body = entity.contents
54
+ context.type = entity.mimetype
55
+ }
56
+
57
+ STORAGE.readFile = async (attachment, encoding) => {
58
+ const id = attachment.split('.')[0]
59
+
60
+ const entity = await getRepository(Attachment).findOne({
61
+ select: ['name', 'contents'],
62
+ where: { id }
63
+ })
64
+
65
+ return await entity.contents
66
+ }
67
+
68
+ STORAGE.generateUploadURL = async (type: string): Promise<{ url: string; fields: { [key: string]: string } }> => {
69
+ const id = crypto.randomUUID()
70
+
71
+ return await {
72
+ url: `/${ATTACHMENT_PATH}`,
73
+ fields: {}
74
+ }
75
+ }
76
+
77
+ logger.info('File Storage is Ready.')
78
+ }
@@ -0,0 +1,78 @@
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').toString('utf-8') /* Because busboy uses latin1 encoding */
18
+
19
+ const stream = createReadStream()
20
+
21
+ mkdirp.sync(uploadDir)
22
+
23
+ id = id || crypto.randomUUID()
24
+ const ext = filename.split('.').pop()
25
+ const path = ext ? resolve(uploadDir, `${id}.${ext}`) : resolve(uploadDir, id)
26
+ const relativePath = path.split('\\').pop().split('/').pop()
27
+ var size: number = 0
28
+
29
+ return new Promise<{
30
+ id: string
31
+ filename: string
32
+ path: string
33
+ size: number
34
+ mimetype: string
35
+ encoding: string
36
+ }>((resolve, reject) =>
37
+ stream
38
+ .on('error', error => {
39
+ if (stream.truncated)
40
+ // Delete the truncated file
41
+ fs.unlinkSync(path)
42
+ reject(error)
43
+ })
44
+ .on('data', chunk => {
45
+ size += chunk.length
46
+ })
47
+ .pipe(fs.createWriteStream(path))
48
+ .on('finish', () => resolve({ id, filename, path: relativePath, size, mimetype, encoding }))
49
+ )
50
+ }
51
+
52
+ STORAGE.deleteFile = async path => {
53
+ const fullpath = resolve(uploadDir, path)
54
+
55
+ await fs.unlink(fullpath, logger.error)
56
+ }
57
+
58
+ STORAGE.sendFile = async (context, attachment, next) => {
59
+ await send(context, attachment, { root: uploadDir })
60
+ }
61
+
62
+ STORAGE.readFile = (attachment, encoding) => {
63
+ const fullpath = resolve(uploadDir, attachment)
64
+
65
+ return fs.readFileSync(fullpath, encoding)
66
+ }
67
+
68
+ STORAGE.generateUploadURL = async (type: string): Promise<{ url: string; fields: { [key: string]: string } }> => {
69
+ const id = crypto.randomUUID()
70
+
71
+ return await {
72
+ url: `/${ATTACHMENT_PATH}`,
73
+ fields: {}
74
+ }
75
+ }
76
+
77
+ logger.info('File Storage is Ready.')
78
+ }
@@ -0,0 +1,139 @@
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').toString('utf-8') /* Because busboy uses latin1 encoding */
40
+
41
+ const stream = createReadStream()
42
+ id = id || crypto.randomUUID()
43
+ const ext = filename.split('.').pop()
44
+ const key = ext ? `${id}.${ext}` : id
45
+
46
+ const upload = new Upload({
47
+ client,
48
+ params: {
49
+ Bucket: STORAGE.bucketName,
50
+ Key: key,
51
+ Body: stream
52
+ }
53
+ })
54
+
55
+ await upload.done()
56
+
57
+ const headObjectCommand = new HeadObjectCommand({
58
+ Bucket: STORAGE.bucketName,
59
+ Key: key
60
+ })
61
+
62
+ const { ContentLength } = await client.send(headObjectCommand)
63
+
64
+ return {
65
+ id,
66
+ path: key,
67
+ filename,
68
+ size: ContentLength,
69
+ mimetype,
70
+ encoding
71
+ }
72
+ }
73
+
74
+ STORAGE.deleteFile = async (path: string) => {
75
+ const command = new DeleteObjectCommand({
76
+ Bucket: STORAGE.bucketName,
77
+ Key: path
78
+ })
79
+
80
+ return await client.send(command)
81
+ }
82
+
83
+ /* TODO Streaming to Streaming 으로 구현하라. */
84
+ STORAGE.sendFile = async (context, attachment, next) => {
85
+ const result = await client.send(
86
+ new GetObjectCommand({
87
+ Bucket: STORAGE.bucketName,
88
+ Key: attachment
89
+ } as GetObjectCommandInput)
90
+ )
91
+
92
+ context.set({
93
+ 'Content-Length': result.ContentLength,
94
+ 'Content-Type': mime.getType(attachment),
95
+ 'Last-Modified': result.LastModified.toUTCString(),
96
+ ETag: result.ETag,
97
+ 'Cache-Control': 'public, max-age=31556926'
98
+ })
99
+
100
+ context.body = result.Body
101
+ }
102
+
103
+ STORAGE.readFile = async (attachment: string, encoding: string) => {
104
+ /*
105
+ * refered to
106
+ * https://transang.me/modern-fetch-and-how-to-get-buffer-output-from-aws-sdk-v3-getobjectcommand/#the-body-type
107
+ */
108
+ const result = await client.send(
109
+ new GetObjectCommand({
110
+ Bucket: STORAGE.bucketName,
111
+ Key: attachment
112
+ } as GetObjectCommandInput)
113
+ )
114
+
115
+ var body = result.Body as Readable
116
+ var buffer = await streamToBuffer(body)
117
+
118
+ switch (encoding) {
119
+ case 'base64':
120
+ return buffer.toString('base64')
121
+ default:
122
+ return await buffer
123
+ }
124
+ }
125
+
126
+ STORAGE.generateUploadURL = async (type: string): Promise<{ url: string; fields: { [key: string]: string } }> => {
127
+ const expiresInMinutes = 1
128
+ const id = crypto.randomUUID()
129
+
130
+ return await createPresignedPost(client, {
131
+ Bucket: STORAGE.bucketName,
132
+ Key: id,
133
+ Expires: expiresInMinutes * 60,
134
+ Conditions: [['eq', '$Content-Type', type]]
135
+ })
136
+ }
137
+
138
+ logger.info('S3 Bucket Storage is Ready.')
139
+ }
@@ -0,0 +1 @@
1
+ export * from './upload-awb'
@@ -0,0 +1,11 @@
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
+ }
@@ -1 +0,0 @@
1
- export declare function normalizeNamesToNFC(): Promise<void>;
@@ -1,24 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeNamesToNFC = normalizeNamesToNFC;
4
- const shell_1 = require("@things-factory/shell");
5
- const attachment_1 = require("./service/attachment/attachment");
6
- async function normalizeNamesToNFC() {
7
- const attachmentRepository = (0, shell_1.getRepository)(attachment_1.Attachment);
8
- // 모든 Attachment 항목을 조회
9
- const attachments = await attachmentRepository.find();
10
- // NFD로 저장된 name을 NFC로 변환하여 업데이트
11
- for (const attachment of attachments) {
12
- if (attachment.name) {
13
- const normalizedName = attachment.name.normalize('NFC');
14
- // name이 NFD로 저장된 경우만 업데이트
15
- if (attachment.name !== normalizedName) {
16
- attachment.name = normalizedName;
17
- await attachmentRepository.save(attachment);
18
- console.log(`Updated name for attachment ID ${attachment.id}: ${normalizedName}`);
19
- }
20
- }
21
- }
22
- console.log('All names have been normalized to NFC.');
23
- }
24
- //# sourceMappingURL=nfc-normalize.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nfc-normalize.js","sourceRoot":"","sources":["../server/nfc-normalize.ts"],"names":[],"mappings":";;AAGA,kDAqBC;AAxBD,iDAAqD;AACrD,gEAA4D;AAErD,KAAK,UAAU,mBAAmB;IACvC,MAAM,oBAAoB,GAAG,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAA;IAEtD,uBAAuB;IACvB,MAAM,WAAW,GAAG,MAAM,oBAAoB,CAAC,IAAI,EAAE,CAAA;IAErD,gCAAgC;IAChC,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC;YACpB,MAAM,cAAc,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;YAEvD,0BAA0B;YAC1B,IAAI,UAAU,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;gBACvC,UAAU,CAAC,IAAI,GAAG,cAAc,CAAA;gBAChC,MAAM,oBAAoB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;gBAC3C,OAAO,CAAC,GAAG,CAAC,kCAAkC,UAAU,CAAC,EAAE,KAAK,cAAc,EAAE,CAAC,CAAA;YACnF,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAA;AACvD,CAAC","sourcesContent":["import { getRepository } from '@things-factory/shell'\nimport { Attachment } from './service/attachment/attachment'\n\nexport async function normalizeNamesToNFC() {\n const attachmentRepository = getRepository(Attachment)\n\n // 모든 Attachment 항목을 조회\n const attachments = await attachmentRepository.find()\n\n // NFD로 저장된 name을 NFC로 변환하여 업데이트\n for (const attachment of attachments) {\n if (attachment.name) {\n const normalizedName = attachment.name.normalize('NFC')\n\n // name이 NFD로 저장된 경우만 업데이트\n if (attachment.name !== normalizedName) {\n attachment.name = normalizedName\n await attachmentRepository.save(attachment)\n console.log(`Updated name for attachment ID ${attachment.id}: ${normalizedName}`)\n }\n }\n }\n\n console.log('All names have been normalized to NFC.')\n}\n"]}