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

teableio / teable / 11385083399

17 Oct 2024 12:37PM UTC coverage: 84.672%. Remained the same
11385083399

push

github

web-flow
feat: comment list image support preview (#998)

* feat: comment list image support preview

* feat: support expand shrinkage comment content

5731 of 6021 branches covered (95.18%)

37863 of 44717 relevant lines covered (84.67%)

1668.8 hits per line

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

7.73
/apps/nestjs-backend/src/features/attachments/plugins/minio.ts
1
/* eslint-disable @typescript-eslint/naming-convention */
4✔
2
import type { Readable as ReadableStream } from 'node:stream';
4✔
3
import { join, resolve } from 'path';
4✔
4
import { BadRequestException, Injectable } from '@nestjs/common';
4✔
5
import { getRandomString } from '@teable/core';
4✔
6
import * as fse from 'fs-extra';
4✔
7
import * as minio from 'minio';
4✔
8
import sharp from 'sharp';
4✔
9
import { IStorageConfig, StorageConfig } from '../../../configs/storage';
4✔
10
import { second } from '../../../utils/second';
4✔
11
import StorageAdapter from './adapter';
4✔
12
import type { IPresignParams, IPresignRes, IRespHeaders } from './types';
4✔
13
import { generateCutImagePath } from './utils';
4✔
14

4✔
15
@Injectable()
4✔
16
export class MinioStorage implements StorageAdapter {
4!
17
  minioClient: minio.Client;
×
18
  minioClientPrivateNetwork: minio.Client;
×
19

×
20
  constructor(@StorageConfig() readonly config: IStorageConfig) {
×
21
    const { endPoint, internalEndPoint, internalPort, port, useSSL, accessKey, secretKey } =
×
22
      this.config.minio;
×
23
    this.minioClient = new minio.Client({
×
24
      endPoint: endPoint!,
×
25
      port: port!,
×
26
      useSSL: useSSL!,
×
27
      accessKey: accessKey!,
×
28
      secretKey: secretKey!,
×
29
    });
×
30
    this.minioClientPrivateNetwork = internalEndPoint
×
31
      ? new minio.Client({
×
32
          endPoint: internalEndPoint,
×
33
          port: internalPort,
×
34
          useSSL: false,
×
35
          accessKey: accessKey!,
×
36
          secretKey: secretKey!,
×
37
        })
×
38
      : this.minioClient;
×
39
    fse.ensureDirSync(StorageAdapter.TEMPORARY_DIR);
×
40
  }
×
41

×
42
  async presigned(
×
43
    bucket: string,
×
44
    dir: string,
×
45
    presignedParams: IPresignParams
×
46
  ): Promise<IPresignRes> {
×
47
    const { tokenExpireIn, uploadMethod } = this.config;
×
48
    const { expiresIn, contentLength, contentType, hash, internal } = presignedParams;
×
49
    const token = getRandomString(12);
×
50
    const filename = hash ?? token;
×
51
    const path = join(dir, filename);
×
52
    const requestHeaders = {
×
53
      'Content-Type': contentType,
×
54
      'Content-Length': contentLength,
×
55
      'response-cache-control': 'max-age=31536000, immutable',
×
56
    };
×
57
    try {
×
58
      const client = internal ? this.minioClientPrivateNetwork : this.minioClient;
×
59
      const url = await client.presignedUrl(
×
60
        uploadMethod,
×
61
        bucket,
×
62
        path,
×
63
        expiresIn ?? second(tokenExpireIn),
×
64
        requestHeaders
×
65
      );
×
66
      return {
×
67
        url,
×
68
        path,
×
69
        token,
×
70
        uploadMethod,
×
71
        requestHeaders,
×
72
      };
×
73
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
×
74
    } catch (e: any) {
×
75
      throw new BadRequestException(`Minio presigned error${e?.message ? `: ${e.message}` : ''}`);
×
76
    }
×
77
  }
×
78

×
79
  private async getShape(bucket: string, objectName: string) {
×
80
    try {
×
81
      const stream = await this.minioClientPrivateNetwork.getObject(bucket, objectName);
×
82
      const metaReader = sharp();
×
83
      const sharpReader = stream.pipe(metaReader);
×
84
      const { width, height } = await sharpReader.metadata();
×
85

×
86
      return {
×
87
        width,
×
88
        height,
×
89
      };
×
90
    } catch (e) {
×
91
      return {};
×
92
    }
×
93
  }
×
94

×
95
  async getObjectMeta(bucket: string, path: string, _token: string) {
×
96
    const objectName = path;
×
97
    const {
×
98
      metaData,
×
99
      size,
×
100
      etag: hash,
×
101
    } = await this.minioClientPrivateNetwork.statObject(bucket, objectName);
×
102
    const mimetype = metaData['content-type'] as string;
×
103
    const url = `/${bucket}/${objectName}`;
×
104
    if (!mimetype?.startsWith('image/')) {
×
105
      return {
×
106
        hash,
×
107
        size,
×
108
        mimetype,
×
109
        url,
×
110
      };
×
111
    }
×
112
    const sharpMeta = await this.getShape(bucket, objectName);
×
113
    return {
×
114
      ...sharpMeta,
×
115
      hash,
×
116
      size,
×
117
      mimetype,
×
118
      url,
×
119
    };
×
120
  }
×
121

×
122
  async getPreviewUrl(
×
123
    bucket: string,
×
124
    path: string,
×
125
    expiresIn: number = second(this.config.urlExpireIn),
×
126
    respHeaders?: IRespHeaders
×
127
  ) {
×
128
    const { 'Content-Disposition': contentDisposition, ...headers } = respHeaders ?? {};
×
129
    return this.minioClient.presignedGetObject(bucket, path, expiresIn, {
×
130
      ...headers,
×
131
      'response-content-disposition': contentDisposition,
×
132
    });
×
133
  }
×
134

×
135
  async uploadFileWidthPath(
×
136
    bucket: string,
×
137
    path: string,
×
138
    filePath: string,
×
139
    metadata: Record<string, string | number>
×
140
  ) {
×
141
    const { etag: hash } = await this.minioClient.fPutObject(bucket, path, filePath, metadata);
×
142
    return {
×
143
      hash,
×
144
      path,
×
145
    };
×
146
  }
×
147

×
148
  async uploadFile(
×
149
    bucket: string,
×
150
    path: string,
×
151
    stream: Buffer | ReadableStream,
×
152
    metadata: Record<string, string | number>
×
153
  ) {
×
154
    const { etag: hash } = await this.minioClientPrivateNetwork.putObject(
×
155
      bucket,
×
156
      path,
×
157
      stream,
×
158
      undefined,
×
159
      metadata
×
160
    );
×
161
    return {
×
162
      hash,
×
163
      path,
×
164
    };
×
165
  }
×
166

×
167
  // minio file exists
×
168
  private async fileExists(bucket: string, path: string) {
×
169
    try {
×
170
      await this.minioClientPrivateNetwork.statObject(bucket, path);
×
171
      return true;
×
172
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
×
173
    } catch (err: any) {
×
174
      if (err.code === 'NoSuchKey' || err.code === 'NotFound') {
×
175
        return false;
×
176
      }
×
177
      throw err;
×
178
    }
×
179
  }
×
180

×
181
  async cutImage(bucket: string, path: string, width: number, height: number) {
×
182
    const newPath = generateCutImagePath(path, width, height);
×
183
    const resizedImagePath = resolve(
×
184
      StorageAdapter.TEMPORARY_DIR,
×
185
      encodeURIComponent(join(bucket, newPath))
×
186
    );
×
187
    if (await this.fileExists(bucket, newPath)) {
×
188
      return newPath;
×
189
    }
×
190

×
191
    const objectName = path;
×
192
    const { metaData } = await this.minioClientPrivateNetwork.statObject(bucket, objectName);
×
193
    const mimetype = metaData['content-type'] as string;
×
194
    if (!mimetype?.startsWith('image/')) {
×
195
      throw new BadRequestException('Invalid image');
×
196
    }
×
197
    const stream = await this.minioClientPrivateNetwork.getObject(bucket, objectName);
×
198
    const metaReader = sharp();
×
199
    const sharpReader = stream.pipe(metaReader);
×
200
    const resizedImage = sharpReader.resize(width, height);
×
201
    await resizedImage.toFile(resizedImagePath);
×
202
    const upload = await this.uploadFileWidthPath(bucket, newPath, resizedImagePath, {
×
203
      'Content-Type': mimetype,
×
204
    });
×
205
    return upload.path;
×
206
  }
×
207
}
×
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