@needle-tools/networking
Version:
Networking backend functionality for Needle Engine
199 lines (190 loc) • 6.71 kB
JavaScript
const S3 = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
class S3Storage {
/**
* @param {{endpoint:string, region:string, bucket:string, accessKeyId:string, accessKey:string, prefix:string}} args
*/
constructor(args) {
this.client = new S3.S3Client({
endpoint: args.endpoint,
region: args.region,
credentials: {
accessKeyId: args.accessKeyId,
secretAccessKey: args.accessKey
}
});
this.bucket = args.bucket;
this.prefix = args.prefix;
}
/**
* @param {string} roomName
* @returns {Promise<{success:boolean, message?:string, state?:any}>}
*/
async loadRoomState(roomName) {
try {
const params = {
Bucket: this.bucket,
Key: `${this.prefix}${roomName}/state.json`
};
const cmd = new S3.GetObjectCommand(params);
const response = await this.client.send(cmd);
const data = await response.Body?.transformToString();
if (typeof data === "string") {
const state = JSON.parse(data);
return {
success: true,
state
};
}
}
catch (e) {
if (e instanceof S3.NoSuchKey) {
return {
success: false,
message: "file does not exist",
};
}
else {
console.error(e);
return {
success: false,
message: "unknown error",
};
}
}
return {
success: false,
message: "unknown error",
};
}
/**
* @param {string} roomName
* @param {any} state
*/
async saveRoomState(roomName, state) {
const params = {
Bucket: this.bucket,
Key: `${this.prefix}${roomName}/state.json`,
Body: JSON.stringify(state),
ContentType: "application/json"
};
const cmd = new S3.PutObjectCommand(params);
try {
await this.client.send(cmd);
}
catch (e) {
console.error(e);
}
}
/**
* @param {string} key
* @returns {Promise<boolean>}
*/
async blobExists(key) {
const params = {
Bucket: this.bucket,
Key: key,
};
const cmd = new S3.HeadObjectCommand(params);
try {
await this.client.send(cmd);
return true;
}
catch (e) {
if (e instanceof S3.NoSuchKey) {
return false;
}
else if (e.name === "NotFound") {
return false;
}
console.error(e);
return false;
}
}
/**
* @param {string} key
* @param {{content_md5:string, content_type:string, content_length:number, filename:string}} data
* @returns {Promise<string | {error:string}>}
*/
async getUploadURL(key, data) {
const { content_md5, content_type, content_length, filename } = data;
if (!key || !filename?.length) {
return { error: "Missing required parameters. Required: key, filename" };
}
// we can cache this indefinitely because the content is immutable
const maxAge = 60 * 60 * 24 * 365;
const command = new S3.PutObjectCommand({
Bucket: this.bucket,
Key: key,
ContentMD5: content_md5,
ContentType: content_type,
// ChecksumSHA256: checksum,
// ChecksumAlgorithm: "SHA256",
ContentLength: content_length,
ContentDisposition: `attachment; filename="${filename}"`,
CacheControl: "public, max-age=" + maxAge,
ServerSideEncryption: "AES256"
});
const url = await getSignedUrl(this.client, command, { expiresIn: 20 }).catch(e => {
return { error: e.message }
});
return url;
}
/**
* @param {string} key
* @returns {Promise<string | {error:string}>} the presigned url if successful otherwise an object with an error message
*/
async getDownloadURL(key) {
const command = new S3.GetObjectCommand({
Bucket: this.bucket,
Key: key
});
const url = await getSignedUrl(this.client, command, { expiresIn: 60 }).catch(e => {
return { error: e.message }
});
return url;
}
}
function tryCreate() {
try {
const dotenv = require("dotenv");
dotenv.config();
const endpoint = process.env.NEEDLE_NETWORKING_S3_ENDPOINT;
if (!endpoint) {
console.error("Can not create S3 storage: missing NEEDLE_NETWORKING_S3_ENDPOINT");
return null;
}
const region = process.env.NEEDLE_NETWORKING_S3_REGION;
if (!region) {
console.error("Can not create S3 storage: missing NEEDLE_NETWORKING_S3_REGION");
return null;
}
const bucket = process.env.NEEDLE_NETWORKING_S3_BUCKET;
if (!bucket) {
console.error("Can not create S3 storage: missing NEEDLE_NETWORKING_S3_BUCKET");
return null;
}
const accessKeyId = process.env.NEEDLE_NETWORKING_S3_ACCESS_KEY_ID;
if (!accessKeyId) {
console.error("Can not create S3 storage: missing NEEDLE_NETWORKING_S3_ACCESS_KEY_ID");
return null;
}
const accessKey = process.env.NEEDLE_NETWORKING_S3_ACCESS_KEY;
if (!accessKey) {
console.error("Can not create S3 storage: missing NEEDLE_NETWORKING_S3_ACCESS_KEY");
return null;
}
const prefix = process.env.NEEDLE_NETWORKING_S3_PREFIX;
if (!prefix || prefix === "/" || prefix === "" || prefix.includes("..") || !prefix?.length) {
console.error("Can not create S3 storage: missing or invalid NEEDLE_NETWORKING_S3_PREFIX");
return null;
}
const s3 = new S3Storage({ endpoint, region, bucket, accessKeyId, accessKey, prefix });
console.log("S3 storage created");
return s3;
} catch (e) {
console.error(e);
}
return null;
}
module.exports = { S3Storage, tryCreateS3Storage: tryCreate };