qmemory
Version:
A comprehensive production-ready Node.js utility library with MongoDB document operations, user ownership enforcement, Express.js HTTP utilities, environment-aware logging, and in-memory storage. Features 96%+ test coverage with comprehensive error handli
153 lines (137 loc) • 3.9 kB
JavaScript
const { Storage, File } = require("@google-cloud/storage");
const { randomUUID } = require("crypto");
const REPLIT_SIDECAR_ENDPOINT = "http://127.0.0.1:1106";
// The object storage client is used to interact with the object storage service.
const objectStorageClient = new Storage({
credentials: {
audience: "replit",
subject_token_type: "access_token",
token_url: `${REPLIT_SIDECAR_ENDPOINT}/token`,
type: "external_account",
credential_source: {
url: `${REPLIT_SIDECAR_ENDPOINT}/credential`,
format: {
type: "json",
subject_token_field_name: "access_token",
},
},
universe_domain: "googleapis.com",
},
projectId: "",
});
class ObjectNotFoundError extends Error {
constructor() {
super("Object not found");
this.name = "ObjectNotFoundError";
Object.setPrototypeOf(this, ObjectNotFoundError.prototype);
}
}
// The object storage service is used to interact with the object storage service.
class ObjectStorageService {
constructor() {
this.objectStorageClient = objectStorageClient;
}
// Gets the public object search paths.
getPublicObjectSearchPaths() {
const pathsStr = process.env.PUBLIC_OBJECT_SEARCH_PATHS || "";
const paths = Array.from(
new Set(
pathsStr
.split(",")
.map((path) => path.trim())
.filter((path) => path.length > 0)
)
);
if (paths.length === 0) {
throw new Error(
"PUBLIC_OBJECT_SEARCH_PATHS not set. Create a bucket in 'Object Storage' " +
"tool and set PUBLIC_OBJECT_SEARCH_PATHS env var (comma-separated paths)."
);
}
return paths;
}
// Gets the private object directory.
getPrivateObjectDir() {
const dir = process.env.PRIVATE_OBJECT_DIR || "";
if (!dir) {
throw new Error(
"PRIVATE_OBJECT_DIR not set. Create a bucket in 'Object Storage' " +
"tool and set PRIVATE_OBJECT_DIR env var."
);
}
return dir;
}
// Gets the upload URL for an object entity.
async getObjectEntityUploadURL() {
const privateObjectDir = this.getPrivateObjectDir();
if (!privateObjectDir) {
throw new Error(
"PRIVATE_OBJECT_DIR not set. Create a bucket in 'Object Storage' " +
"tool and set PRIVATE_OBJECT_DIR env var."
);
}
const objectId = randomUUID();
const fullPath = `${privateObjectDir}/uploads/${objectId}`;
const { bucketName, objectName } = parseObjectPath(fullPath);
// Sign URL for PUT method with TTL
return signObjectURL({
bucketName,
objectName,
method: "PUT",
ttlSec: 900,
});
}
}
function parseObjectPath(path) {
if (!path.startsWith("/")) {
path = `/${path}`;
}
const pathParts = path.split("/");
if (pathParts.length < 3) {
throw new Error("Invalid path: must contain at least a bucket name");
}
const bucketName = pathParts[1];
const objectName = pathParts.slice(2).join("/");
return {
bucketName,
objectName,
};
}
async function signObjectURL({
bucketName,
objectName,
method,
ttlSec,
}) {
const request = {
bucket_name: bucketName,
object_name: objectName,
method,
expires_at: new Date(Date.now() + ttlSec * 1000).toISOString(),
};
const response = await fetch(
`${REPLIT_SIDECAR_ENDPOINT}/object-storage/signed-object-url`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
}
);
if (!response.ok) {
throw new Error(
`Failed to sign object URL, errorcode: ${response.status}, ` +
`make sure you're running on Replit`
);
}
const { signed_url: signedURL } = await response.json();
return signedURL;
}
module.exports = {
ObjectStorageService,
ObjectNotFoundError,
objectStorageClient,
parseObjectPath,
signObjectURL
};