-
Daniel Göbel authored
#113
Daniel Göbel authored#113
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;
});
},
},
});