mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
159 lines
6.0 KiB
JavaScript
159 lines
6.0 KiB
JavaScript
// services/s3.service.js
|
||
//
|
||
// S3-compatible storage service targeting Garage (self-hosted).
|
||
// Exposes the same interface as chibisafe.service.js:
|
||
// uploadFile({ buffer, originalname, mimetype, ownerType }) → { url, uuid }
|
||
// deleteFile(key)
|
||
//
|
||
// Required .env vars:
|
||
// S3_ENDPOINT – http://127.0.0.1:3900
|
||
// S3_REGION – garage
|
||
// S3_ACCESS_KEY
|
||
// S3_SECRET_KEY
|
||
// S3_BUCKET – philproperties
|
||
// S3_PUBLIC_URL – https://garage.philproperties.com
|
||
|
||
const { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadBucketCommand } = require("@aws-sdk/client-s3");
|
||
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||
const crypto = require("crypto");
|
||
const path = require("path");
|
||
|
||
// ─── Client ───────────────────────────────────────────────────────────────────
|
||
|
||
const s3 = new S3Client({
|
||
endpoint: process.env.S3_ENDPOINT,
|
||
region: process.env.S3_REGION || "garage",
|
||
credentials: {
|
||
accessKeyId: process.env.S3_ACCESS_KEY,
|
||
secretAccessKey: process.env.S3_SECRET_KEY,
|
||
},
|
||
forcePathStyle: true, // required — Garage does not support DNS-style bucket addressing
|
||
});
|
||
|
||
const DEFAULT_BUCKET = process.env.S3_BUCKET || "philproperties";
|
||
const PUBLIC_URL = (process.env.S3_PUBLIC_URL || "").replace(/\/$/, "");
|
||
|
||
// ─── Key prefix map ───────────────────────────────────────────────────────────
|
||
//
|
||
// ownerType mirrors what the controller already passes to chibisafe
|
||
// (image, video, audio, document, avatar, thumbnail).
|
||
// Maps to folder prefixes inside the bucket.
|
||
//
|
||
const PREFIX_MAP = {
|
||
image: "images",
|
||
video: "videos",
|
||
audio: "audios",
|
||
document: "documents",
|
||
avatar: "avatars",
|
||
thumbnail: "thumbnails",
|
||
};
|
||
|
||
function resolvePrefix(ownerType = "image") {
|
||
return PREFIX_MAP[ownerType] || "others";
|
||
}
|
||
|
||
function resolveExtension(originalname = "") {
|
||
return path.extname(originalname).replace(".", "").toLowerCase() || "bin";
|
||
}
|
||
|
||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||
|
||
// Builds the S3 object key: {prefix}/{uuid}.{ext}
|
||
// e.g. images/3f2a1b4c-uuid.jpg
|
||
function buildKey(originalname, ownerType) {
|
||
const prefix = resolvePrefix(ownerType);
|
||
const ext = resolveExtension(originalname);
|
||
const uuid = crypto.randomUUID();
|
||
return `${prefix}/${uuid}.${ext}`;
|
||
}
|
||
|
||
// Builds the public URL for a stored object.
|
||
// Garage path-style: {S3_PUBLIC_URL}/{bucket}/{key}
|
||
// e.g. https://garage.philproperties.com/philproperties/images/uuid.jpg
|
||
function buildPublicUrl(key, bucket = DEFAULT_BUCKET) {
|
||
return `${PUBLIC_URL}/${bucket}/${key}`;
|
||
}
|
||
|
||
// ─── uploadFile ───────────────────────────────────────────────────────────────
|
||
//
|
||
// Matches chibisafe.service.js interface exactly.
|
||
// input: { buffer, originalname, mimetype, ownerType? }
|
||
// output: { url, uuid } ← uuid = the S3 key, stored as storage_key in DB
|
||
//
|
||
async function uploadFile({ buffer, originalname, mimetype, ownerType = "image" }) {
|
||
if (!buffer) {
|
||
throw Object.assign(new Error("File buffer is required for S3 uploads."), { status: 400 });
|
||
}
|
||
|
||
const bucket = DEFAULT_BUCKET;
|
||
const key = buildKey(originalname, ownerType);
|
||
|
||
await s3.send(new PutObjectCommand({
|
||
Bucket: bucket,
|
||
Key: key,
|
||
Body: buffer,
|
||
ContentType: mimetype,
|
||
}));
|
||
|
||
return {
|
||
url: buildPublicUrl(key, bucket),
|
||
uuid: key, // used as storage_key in DB — mirrors chibi_uuid usage
|
||
};
|
||
}
|
||
|
||
// ─── deleteFile ───────────────────────────────────────────────────────────────
|
||
//
|
||
// Matches chibisafe.service.js interface.
|
||
// input: key — the value stored in storage_key column (e.g. "images/uuid.jpg")
|
||
//
|
||
async function deleteFile(key) {
|
||
if (!key) return;
|
||
await s3.send(new DeleteObjectCommand({
|
||
Bucket: DEFAULT_BUCKET,
|
||
Key: key,
|
||
}));
|
||
}
|
||
|
||
// ─── getSignedDownloadUrl ─────────────────────────────────────────────────────
|
||
//
|
||
// Generates a short-lived pre-signed GET URL.
|
||
// Useful if you ever need gated access to private assets (is_public = false).
|
||
//
|
||
async function getSignedDownloadUrl(key, expiresInSeconds = 3600) {
|
||
return getSignedUrl(
|
||
s3,
|
||
new GetObjectCommand({ Bucket: DEFAULT_BUCKET, Key: key }),
|
||
{ expiresIn: expiresInSeconds }
|
||
);
|
||
}
|
||
|
||
// ─── getObjectStream ────────────────────────────────────────────────────────
|
||
//
|
||
// Fetches an S3 object and returns its readable stream + metadata, for use
|
||
// in a server-side download proxy (Content-Disposition: attachment).
|
||
//
|
||
// input: key — the storage_key (e.g. "images/uuid.jpg")
|
||
// output: { stream, contentType, contentLength }
|
||
//
|
||
async function getObjectStream(key) {
|
||
if (!key) {
|
||
throw Object.assign(new Error("Storage key is required."), { status: 400 });
|
||
}
|
||
|
||
const result = await s3.send(new GetObjectCommand({
|
||
Bucket: DEFAULT_BUCKET,
|
||
Key: key,
|
||
}));
|
||
|
||
return {
|
||
stream: result.Body, // Node.js Readable stream
|
||
contentType: result.ContentType,
|
||
contentLength: result.ContentLength,
|
||
};
|
||
}
|
||
|
||
async function ping() {
|
||
await s3.send(new HeadBucketCommand({ Bucket: DEFAULT_BUCKET }));
|
||
}
|
||
|
||
module.exports = { uploadFile, deleteFile, getSignedDownloadUrl, getObjectStream, ping }; |