import { ABC, Path, Compress, FS, Colors } from "../deps.ts"; import bucket from "../s3.ts"; import { isValidPackageName, basicauth, isValidFullVersion } from "../utils.ts"; import db, { IPackage } from "../db.ts"; import { v4 } from "https://deno.land/std/uuid/mod.ts"; export default function api(g: ABC.Group) { const cacheControl = (next: ABC.HandlerFunc) => (ctx: ABC.Context) => { ctx.response.headers.set("cache-control", "private"); return next(ctx); }; g.get( "/", (ctx) => { return { version: "1" }; }, cacheControl ); // g.post("/getapikey", getApiKey, basicauth("api")); g.post("/package/:name", uploadPackage, cacheControl, basicauth("api")); } // async function getApiKey(ctx: ABC.Context) { // const key = v4.generate(); // await db.api_key.insert({ // user: ctx.customContext.user, // key, // createdAt: new Date(), // lastAccess: undefined, // lastIP: undefined, // }); // return { // key, // }; // } async function uploadPackage(ctx: ABC.Context) { const reqId = v4.generate(); const filename = "./tmp/" + reqId + ".tar"; const folder = "./tmp/" + reqId; try { const packageName = ctx.params.name.toLowerCase(); if (!isValidPackageName(packageName)) { return { success: false, message: "Invalid package name", }; } console.log("Writing body to tmp file:", filename); const file = await Deno.open(filename, { write: true, create: true, }); await Deno.copy(ctx.request.body, file); file.close(); console.log("Create unpacked folder"); await Deno.mkdir(folder); console.log("Uncompressing tar"); await Compress.Tar.uncompress(filename, folder); console.log("Reading meta.json"); const fileContent = await Deno.readFile(Path.join(folder, "meta.json")); console.log("Parsing meta.json"); const meta = JSON.parse(new TextDecoder().decode(fileContent)); console.log("Checking meta.json"); if (!meta?.version) { return { success: false, message: "No version available in meta.json", }; } const packageVersion = meta.version; console.log("Checking correct version"); if (!isValidFullVersion(packageVersion)) { return { success: false, message: "Invalid version. Version must be in format: 0.0.0", }; return; } console.log("Checking for previous uploads"); let packageMeta = await db.package.findOne({ name: packageName }); console.log(meta, packageMeta); if (!packageMeta) { packageMeta = { name: packageName, owner: ctx.customContext.user, description: meta.description, deprecated: false, versions: [], }; await db.package.insert(packageMeta); } console.log("Check if version was uploaded before"); if (packageMeta.versions.find((e) => e === meta.version)) { return { success: false, message: "Version was already uploaded!", }; } const bucketBase = "packages/" + packageName + "/" + packageVersion + "/"; console.log("Uploading files to S3"); const walker = FS.walk(folder, { includeFiles: true, includeDirs: false, }); for await (const file of walker) { const relative = Path.relative(folder, file.path).replace("\\", "/"); console.log("Normalised path:", relative.replace("\\", "/")); const bucketPath = (bucketBase + relative).replace(/@/g, "ยง"); const body = await Deno.readAll(await Deno.open(file.path)); await bucket.putObject(bucketPath, body, {}); } console.log("Setting new live version"); //TODO: Better option, since this could error whith multiple uploads to the same package at the same time await db.package.update( { name: packageName }, { $set: { versions: [...packageMeta.versions, packageVersion], description: meta.description || packageMeta.description, deprecated: meta.deprecated === true, }, } ); console.log("Finished successfully"); return { success: true, }; } catch (err) { console.error( Colors.red("Error while processing newly uploaded package") ); console.error(err); return { success: false, message: err.message, }; } finally { console.log("Cleanup"); await Deno.remove(filename).catch(console.error); await Deno.remove(folder, { recursive: true }).catch(console.error); } }