Skip to content
Snippets Groups Projects
s3objects.ts 8.45 KiB
import { defineStore } from "pinia";
import type { _Object as S3Object, HeadObjectOutput } from "@aws-sdk/client-s3";
import {
  AbortMultipartUploadCommand,
  CopyObjectCommand,
  GetObjectCommand,
  HeadObjectCommand,
  PutObjectCommand,
  S3Client,
} from "@aws-sdk/client-s3";
import {
  paginateListObjectsV2,
  DeleteObjectCommand,
  DeleteObjectsCommand,
} from "@aws-sdk/client-s3";
import { environment } from "@/environment";
import type { S3Key } from "@/client/s3proxy";
import { useBucketStore } from "@/stores/buckets";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import dayjs from "dayjs";
import { Upload } from "@aws-sdk/lib-storage";
import type { Progress } from "@aws-sdk/lib-storage";
import type { AbortController } from "@smithy/types";

export const useS3ObjectStore = defineStore({
  id: "s3objects",
  state: () =>
    ({
      objectMapping: {},
      objectMetaMapping: {},
      client: new S3Client({
        region: "us-east-1",
        endpoint: environment.S3_URL,
        forcePathStyle: true,
        credentials: {
          accessKeyId: "",
          secretAccessKey: "",
        },
      }),
    }) as {
      objectMapping: Record<string, S3Object[]>;
      objectMetaMapping: Record<string, HeadObjectOutput>;
      client: S3Client;
    },
  getters: {
    getPresignedUrl(): (bucketName: string, key: string) => Promise<string> {
      return (bucketName, key) => {
        const keySplit = key.split("/");
        const command = new GetObjectCommand({
          Bucket: bucketName,
          Key: key,
          ResponseContentDisposition: `attachment; filename=${
            keySplit[keySplit.length - 1]
          }`,
        });
        return getSignedUrl(this.client, command, {
          expiresIn: 30,
        });
      };
    },
    getMeta(): (bucketName: string) => [number, number] {
      // Compute the number of objects and the cumulative size of all objects in a bucket
      return (bucketName) => {
        return [
          this.objectMapping[bucketName]?.length ?? 0,
          this.objectMapping[bucketName]?.reduce(
            (acc, obj) => acc + (obj.Size ?? 0),
            0,
          ) ?? 0,
        ];
      };
    },
  },
  actions: {
    _pushObject(bucketName: string, newObj: S3Object) {
      if (this.objectMapping[bucketName] == undefined) {
        this.fetchS3Objects(bucketName);
      } else {
        const objIndex = this.objectMapping[bucketName].findIndex(
          (obj) => obj.Key === newObj.Key,
        );
        if (objIndex > -1) {
          this.objectMapping[bucketName][objIndex] = newObj;
        } else {
          this.objectMapping[bucketName].push(newObj);
        }
      }
    },
    updateS3Client(s3Key: S3Key) {
      this.client = new S3Client({
        region: "us-east-1",
        endpoint: environment.S3_URL,
        forcePathStyle: true,
        credentials: {
          accessKeyId: s3Key.access_key,
          secretAccessKey: s3Key.secret_key,
        },
      });
    },
    async fetchS3Objects(
      bucketName: string,
      prefix?: string,
      onFinally?: () => void,
    ): Promise<S3Object[]> {
      if (this.objectMapping[bucketName] != undefined) {
        onFinally?.();
      }
      const pag = paginateListObjectsV2(
        { client: this.client },
        { Bucket: bucketName, Prefix: prefix },
      );
      const objs: S3Object[] = [];
      try {
        for await (const page of pag) {
          objs.push(...(page.Contents ?? []));
        }
        this.objectMapping[bucketName] = objs;
      } finally {
        onFinally?.();
      }
      return objs;
    },
    fetchS3ObjectMeta(
      bucketName: string,
      key: string,
      onFinally?: () => void,
    ): Promise<HeadObjectOutput> {
      const identifier = bucketName + "/" + key;
      if (this.objectMetaMapping[identifier]) {
        onFinally?.();
      }
      const command = new HeadObjectCommand({
        Bucket: bucketName,
        Key: key,
      });
      return this.client
        .send(command)
        .then((resp) => {
          this.objectMetaMapping[identifier] = resp;
          return resp;
        })
        .finally(onFinally);
    },
    deleteObject(bucketName: string, key: string): Promise<void> {
      const command = new DeleteObjectCommand({
        Bucket: bucketName,
        Key: key,
      });
      return this.client
        .send(command)
        .then(() => {
          const bucketRepository = useBucketStore();
          bucketRepository.fetchBucket(bucketName);
          if (this.objectMapping[bucketName] == undefined) {
            this.fetchS3Objects(bucketName);
          } else {
            this.objectMapping[bucketName] = this.objectMapping[
              bucketName
            ].filter((obj) => obj.Key !== key);
          }
        })
        .catch((err) => {
          console.error(err);
        });
    },
    deleteObjectsWithPrefix(bucketName: string, prefix: string): Promise<void> {
      if (this.objectMapping[bucketName] == undefined) {
        return Promise.resolve();
      }
      const command = new DeleteObjectsCommand({
        Bucket: bucketName,
        Delete: {
          Objects: this.objectMapping[bucketName]
            .filter((obj) => obj.Key?.startsWith(prefix))
            .map((obj) => {
              return { Key: obj.Key };
            }),
        },
      });
      return this.client.send(command).then(() => {
        const bucketRepository = useBucketStore();
        bucketRepository.fetchBucket(bucketName);
        if (this.objectMapping[bucketName] == undefined) {
          this.fetchS3Objects(bucketName);
        } else {
          this.objectMapping[bucketName] = this.objectMapping[
            bucketName
          ].filter((obj) => !obj.Key?.startsWith(prefix));
        }
      });
    },
    copyObject(
      srcBucket: string,
      srcObject: S3Object,
      destBucket: string,
      destKey: string,
      abortController?: AbortController,
    ): Promise<S3Object> {
      if (srcObject.Key == undefined) {
        return Promise.resolve({});
      }
      const command = new CopyObjectCommand({
        Bucket: destBucket,
        CopySource: encodeURI(`/${srcBucket}/${srcObject.Key}`),
        Key: destKey,
      });
      return this.client
        .send(command, { abortSignal: abortController?.signal })
        .then(() => {
          const newObj = {
            Key: destKey,
            Size: srcObject.Size,
            LastModified: dayjs().toDate(),
          };
          this._pushObject(destBucket, newObj);
          return newObj;
        });
    },
    async uploadObjectFile(
      bucketName: string,
      key: string,
      file: File,
      onProgress?: (progress: Progress) => void,
      abortController?: AbortController,
    ): Promise<S3Object> {
      const parallelUploads3 = new Upload({
        client: this.client,
        params: {
          Bucket: bucketName,
          Body: file,
          ContentType: file.type,
          Key: key,
        },
        queueSize: 4, // optional concurrency configuration
        leavePartsOnError: false, // optional manually handle dropped parts
        abortController: abortController,
      });
      if (onProgress != undefined) {
        parallelUploads3.on("httpUploadProgress", onProgress);
      }
      try {
        await parallelUploads3.done();
      } catch (e) {
        // if there is an error with the multipart upload, send an abort multipart upload command
        const uploadObject = JSON.parse(JSON.stringify(parallelUploads3));
        if (uploadObject["isMultiPart"] && uploadObject["uploadId"]) {
          await this.client.send(
            new AbortMultipartUploadCommand({
              // AbortMultipartUploadRequest
              Bucket: bucketName, // required
              Key: key, // required
              UploadId: uploadObject["uploadId"], // required
            }),
          );
        }
        throw e;
      }
      const newObj = {
        Key: key,
        Size: file.size ?? 0,
        LastModified: dayjs().toDate(),
      };
      this._pushObject(bucketName, newObj);
      return newObj;
    },
    createFolder(bucketName: string, key: string): Promise<S3Object> {
      const command = new PutObjectCommand({
        Bucket: bucketName,
        Body: "",
        ContentType: "text/plain",
        Key: key,
      });
      return this.client.send(command).then(() => {
        const newObj = {
          Key: key,
          Size: 0,
          LastModified: dayjs().toDate(),
        };
        this._pushObject(bucketName, newObj);
        return newObj;
      });
    },
  },
});