• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 10317318504

09 Aug 2024 09:56AM CUT coverage: 82.669%. First build
10317318504

push

github

web-flow
build: parallel building amd and arm (#809)

* build: test new martix

* fix: skip app

* chore: build all

4426 of 4647 branches covered (95.24%)

29326 of 35474 relevant lines covered (82.67%)

1241.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

8.85
/apps/nestjs-backend/src/features/attachments/plugins/s3.ts
1
/* eslint-disable sonarjs/no-duplicate-string */
4✔
2
/* eslint-disable @typescript-eslint/naming-convention */
4✔
3
import { join } from 'path';
4✔
4
import type { Readable } from 'stream';
4✔
5
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
4✔
6
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
4✔
7
import { BadRequestException, Injectable } from '@nestjs/common';
4✔
8
import { getRandomString } from '@teable/core';
4✔
9
import ms from 'ms';
4✔
10
import sharp from 'sharp';
4✔
11
import { IStorageConfig, StorageConfig } from '../../../configs/storage';
4✔
12
import { second } from '../../../utils/second';
4✔
13
import type StorageAdapter from './adapter';
4✔
14
import type { IPresignParams, IPresignRes, IObjectMeta, IRespHeaders } from './types';
4✔
15

4✔
16
@Injectable()
4✔
17
export class S3Storage implements StorageAdapter {
4!
18
  private s3Client: S3Client;
×
19

×
20
  constructor(@StorageConfig() readonly config: IStorageConfig) {
×
21
    const { endpoint, region, accessKey, secretKey } = this.config.s3;
×
22
    this.checkConfig();
×
23
    this.s3Client = new S3Client({
×
24
      region,
×
25
      endpoint,
×
26
      credentials: {
×
27
        accessKeyId: accessKey,
×
28
        secretAccessKey: secretKey,
×
29
      },
×
30
    });
×
31
  }
×
32

×
33
  private checkConfig() {
×
34
    const { tokenExpireIn } = this.config;
×
35
    if (ms(tokenExpireIn) >= ms('7d')) {
×
36
      throw new BadRequestException('Token expire in must be more than 7 days');
×
37
    }
×
38
    if (!this.config.s3.region) {
×
39
      throw new BadRequestException('S3 region is required');
×
40
    }
×
41
    if (!this.config.s3.endpoint) {
×
42
      throw new BadRequestException('S3 endpoint is required');
×
43
    }
×
44
    if (!this.config.s3.accessKey) {
×
45
      throw new BadRequestException('S3 access key is required');
×
46
    }
×
47
    if (!this.config.s3.secretKey) {
×
48
      throw new BadRequestException('S3 secret key is required');
×
49
    }
×
50
    if (this.config.uploadMethod.toLocaleLowerCase() !== 'put') {
×
51
      throw new BadRequestException('S3 upload method must be put');
×
52
    }
×
53
  }
×
54

×
55
  async presigned(bucket: string, dir: string, params: IPresignParams): Promise<IPresignRes> {
×
56
    try {
×
57
      const { tokenExpireIn, uploadMethod } = this.config;
×
58
      const { expiresIn, contentLength, contentType, hash } = params;
×
59

×
60
      const token = getRandomString(12);
×
61
      const filename = hash ?? token;
×
62
      const path = join(dir, filename);
×
63

×
64
      const command = new PutObjectCommand({
×
65
        Bucket: bucket,
×
66
        Key: path,
×
67
        ContentType: contentType,
×
68
        ContentLength: contentLength,
×
69
      });
×
70

×
71
      const url = await getSignedUrl(this.s3Client, command, {
×
72
        expiresIn: expiresIn ?? second(tokenExpireIn),
×
73
      });
×
74

×
75
      const requestHeaders = {
×
76
        'Content-Type': contentType,
×
77
        'Content-Length': contentLength,
×
78
      };
×
79

×
80
      return {
×
81
        url,
×
82
        path,
×
83
        token,
×
84
        uploadMethod,
×
85
        requestHeaders,
×
86
      };
×
87
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
×
88
    } catch (e: any) {
×
89
      throw new BadRequestException(`S3 presigned error${e?.message ? `: ${e.message}` : ''}`);
×
90
    }
×
91
  }
×
92
  async getObjectMeta(bucket: string, path: string): Promise<IObjectMeta> {
×
93
    const url = `/${bucket}/${path}`;
×
94
    const command = new GetObjectCommand({
×
95
      Bucket: bucket,
×
96
      Key: path,
×
97
    });
×
98
    const {
×
99
      ContentLength: size,
×
100
      ContentType: mimetype,
×
101
      ETag: hash,
×
102
      Body: stream,
×
103
    } = await this.s3Client.send(command);
×
104
    if (!size || !mimetype || !hash || !stream) {
×
105
      throw new BadRequestException('Invalid object meta');
×
106
    }
×
107
    if (!mimetype?.startsWith('image/')) {
×
108
      return {
×
109
        hash,
×
110
        size,
×
111
        mimetype,
×
112
        url,
×
113
      };
×
114
    }
×
115
    const metaReader = sharp();
×
116
    const sharpReader = (stream as Readable).pipe(metaReader);
×
117
    const { width, height } = await sharpReader.metadata();
×
118

×
119
    return {
×
120
      hash,
×
121
      url,
×
122
      size,
×
123
      mimetype,
×
124
      width,
×
125
      height,
×
126
    };
×
127
  }
×
128
  getPreviewUrl(
×
129
    bucket: string,
×
130
    path: string,
×
131
    expiresIn: number = second(this.config.urlExpireIn),
×
132
    respHeaders?: IRespHeaders
×
133
  ): Promise<string> {
×
134
    const command = new GetObjectCommand({
×
135
      Bucket: bucket,
×
136
      Key: path,
×
137
      ResponseContentType: respHeaders?.['Content-Type'],
×
138
      ResponseContentDisposition: respHeaders?.['Content-Disposition'],
×
139
    });
×
140

×
141
    return getSignedUrl(this.s3Client, command, {
×
142
      expiresIn: expiresIn ?? second(this.config.tokenExpireIn),
×
143
    });
×
144
  }
×
145
  uploadFileWidthPath(
×
146
    bucket: string,
×
147
    path: string,
×
148
    filePath: string,
×
149
    metadata: Record<string, unknown>
×
150
  ) {
×
151
    const command = new PutObjectCommand({
×
152
      Bucket: bucket,
×
153
      Key: path,
×
154
      Body: filePath,
×
155
      ContentType: metadata['Content-Type'] as string,
×
156
      ContentLength: metadata['Content-Length'] as number,
×
157
      ContentDisposition: metadata['Content-Disposition'] as string,
×
158
      ContentEncoding: metadata['Content-Encoding'] as string,
×
159
      ContentLanguage: metadata['Content-Language'] as string,
×
160
      ContentMD5: metadata['Content-MD5'] as string,
×
161
    });
×
162

×
163
    return this.s3Client.send(command).then((res) => ({
×
164
      hash: res.ETag!,
×
165
      path,
×
166
    }));
×
167
  }
×
168

×
169
  uploadFile(
×
170
    bucket: string,
×
171
    path: string,
×
172
    stream: Buffer | Readable,
×
173
    metadata?: Record<string, unknown>
×
174
  ) {
×
175
    const command = new PutObjectCommand({
×
176
      Bucket: bucket,
×
177
      Key: path,
×
178
      Body: stream,
×
179
      ContentType: metadata?.['Content-Type'] as string,
×
180
      ContentLength: metadata?.['Content-Length'] as number,
×
181
      ContentDisposition: metadata?.['Content-Disposition'] as string,
×
182
      ContentEncoding: metadata?.['Content-Encoding'] as string,
×
183
      ContentLanguage: metadata?.['Content-Language'] as string,
×
184
      ContentMD5: metadata?.['Content-MD5'] as string,
×
185
    });
×
186

×
187
    return this.s3Client.send(command).then((res) => ({
×
188
      hash: res.ETag!,
×
189
      path,
×
190
    }));
×
191
  }
×
192
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc