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

teableio / teable / 11342251885

15 Oct 2024 08:30AM CUT coverage: 84.693%. First build
11342251885

Pull #986

github

web-flow
Merge d647eb238 into 7794225a9
Pull Request #986: feat: support excel form view

5712 of 6020 branches covered (94.88%)

54 of 55 new or added lines in 5 files covered. (98.18%)

37879 of 44725 relevant lines covered (84.69%)

1623.76 hits per line

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

8.12
/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.minioClient.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 { metaData, size, etag: hash } = await this.minioClient.statObject(bucket, objectName);
×
98
    const mimetype = metaData['content-type'] as string;
×
99
    const url = `/${bucket}/${objectName}`;
×
100
    if (!mimetype?.startsWith('image/')) {
×
101
      return {
×
102
        hash,
×
103
        size,
×
104
        mimetype,
×
105
        url,
×
106
      };
×
107
    }
×
108
    const sharpMeta = await this.getShape(bucket, objectName);
×
109
    return {
×
110
      ...sharpMeta,
×
111
      hash,
×
112
      size,
×
113
      mimetype,
×
114
      url,
×
115
    };
×
116
  }
×
117

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

×
131
  async uploadFileWidthPath(
×
132
    bucket: string,
×
133
    path: string,
×
134
    filePath: string,
×
135
    metadata: Record<string, unknown>
×
136
  ) {
×
137
    const { etag: hash } = await this.minioClient.fPutObject(bucket, path, filePath, metadata);
×
138
    return {
×
139
      hash,
×
140
      path,
×
141
    };
×
142
  }
×
143

×
144
  async uploadFile(
×
145
    bucket: string,
×
146
    path: string,
×
147
    stream: Buffer | ReadableStream,
×
148
    metadata?: Record<string, unknown>
×
149
  ) {
×
150
    const { etag: hash } = await this.minioClient.putObject(bucket, path, stream, metadata);
×
151
    return {
×
152
      hash,
×
153
      path,
×
154
    };
×
155
  }
×
156

×
157
  // minio file exists
×
158
  private async fileExists(bucket: string, path: string) {
×
159
    try {
×
160
      await this.minioClient.statObject(bucket, path);
×
161
      return true;
×
162
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
×
163
    } catch (err: any) {
×
164
      if (err.code === 'NoSuchKey' || err.code === 'NotFound') {
×
165
        return false;
×
166
      }
×
167
      throw err;
×
168
    }
×
169
  }
×
170

×
171
  async cutImage(bucket: string, path: string, width: number, height: number) {
×
172
    const newPath = generateCutImagePath(path, width, height);
×
173
    const resizedImagePath = resolve(
×
174
      StorageAdapter.TEMPORARY_DIR,
×
175
      encodeURIComponent(join(bucket, newPath))
×
176
    );
×
177
    if (await this.fileExists(bucket, newPath)) {
×
178
      return newPath;
×
179
    }
×
180

×
181
    const objectName = path;
×
182
    const { metaData } = await this.minioClient.statObject(bucket, objectName);
×
183
    const mimetype = metaData['content-type'] as string;
×
184
    if (!mimetype?.startsWith('image/')) {
×
185
      throw new BadRequestException('Invalid image');
×
186
    }
×
187
    const stream = await this.minioClient.getObject(bucket, objectName);
×
188
    const metaReader = sharp();
×
189
    const sharpReader = stream.pipe(metaReader);
×
190
    const resizedImage = sharpReader.resize(width, height);
×
191
    await resizedImage.toFile(resizedImagePath);
×
192
    const upload = await this.uploadFileWidthPath(bucket, newPath, resizedImagePath, {
×
193
      'Content-Type': mimetype,
×
194
    });
×
195
    return upload.path;
×
196
  }
×
197
}
×
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