First Commit of registry itself
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
34e482615d
commit
6fc258cf3a
3
.drone.status
Normal file
3
.drone.status
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"url": "https://drone.hibas123.de/Deno/DenReg/"
|
||||||
|
}
|
17
.drone.yml
Normal file
17
.drone.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build denreg registry docker image
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
auto_tag: true
|
||||||
|
repo: hibas123.azurecr.io/denreg
|
||||||
|
registry: hibas123.azurecr.io
|
||||||
|
dockerfile: registry/Dockerfile
|
||||||
|
debug: true
|
@ -5,3 +5,5 @@ end_of_line = lf
|
|||||||
indent_size = 3
|
indent_size = 3
|
||||||
indent_style = space
|
indent_style = space
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
[*.yml]
|
||||||
|
indent_size = 2
|
||||||
|
1
registry/.gitignore
vendored
Normal file
1
registry/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
tmp/
|
33
registry/Dockerfile
Normal file
33
registry/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
FROM debian:bullseye-slim AS builder
|
||||||
|
|
||||||
|
ENV DENO_VERSION=1.2.1
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y curl zip
|
||||||
|
|
||||||
|
RUN curl -fsSL https://github.com/denoland/deno/releases/download/v${DENO_VERSION}/deno-x86_64-unknown-linux-gnu.zip --output deno.zip
|
||||||
|
RUN unzip deno.zip
|
||||||
|
RUN rm deno.zip
|
||||||
|
RUN chmod 777 deno
|
||||||
|
RUN chmod +x deno
|
||||||
|
RUN mv deno /usr/bin/deno
|
||||||
|
|
||||||
|
FROM debian:bullseye-slim
|
||||||
|
|
||||||
|
COPY --from=builder /usr/bin/deno /usr/bin/deno
|
||||||
|
|
||||||
|
RUN ls /usr/bin/deno
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY src/deps.ts /app/src/deps.ts
|
||||||
|
|
||||||
|
RUN /usr/bin/deno cache --unstable src/deps.ts
|
||||||
|
|
||||||
|
ADD src /app/src
|
||||||
|
|
||||||
|
RUN /usr/bin/deno cache --unstable src/registry.ts
|
||||||
|
|
||||||
|
VOLUME [ "/data" ]
|
||||||
|
ENTRYPOINT [ "/usr/bin/deno", "run", "-A", "--unstable", "/app/src/registry.ts" ]
|
11
registry/data/config.ini
Normal file
11
registry/data/config.ini
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[s3]
|
||||||
|
endpoint=https://minio.hibas123.de
|
||||||
|
bucket=deno-registry
|
||||||
|
accessKey=h6MlrWqvhR1V2id1Q5Ei
|
||||||
|
secretKey=zQaJkX9fXvouCVtnH0cSgJyJojsrIiCQPrsQfdnI
|
||||||
|
|
||||||
|
[user.hibas123]
|
||||||
|
password=asd
|
||||||
|
|
||||||
|
[user.asd]
|
||||||
|
password=asd
|
1
registry/data/db.json
Normal file
1
registry/data/db.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"x":[{"name":"test","versions":["0.0.1"],"_id":"7ee9bbb0-ce89-11ea-bfc4-7fc1af1f206a"},{"name":"@denreg-cli","versions":["0.0.1","0.0.2","1.2.5","1.3.5"],"_id":"5e096590-cf32-11ea-905e-2929f303405b"}]}
|
5
registry/dev.sh
Executable file
5
registry/dev.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
deno run -A https://raw.githubusercontent.com/hibas123/denovamon/master/denovamon.ts start --ignore="db.json,tmp" --command="deno run -A --unstable --importmap import_map.json src/registry.ts"
|
||||||
|
|
||||||
|
# deno run -A --unstable --importmap import_map.json registry.ts
|
5
registry/import_map.json
Normal file
5
registry/import_map.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"https://deno.land/std@0.60.0/": "https://deno.land/std@0.62.0/"
|
||||||
|
}
|
||||||
|
}
|
10
registry/src/config.ts
Normal file
10
registry/src/config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Ini } from "./deps.ts";
|
||||||
|
|
||||||
|
const config =
|
||||||
|
Ini.decode(
|
||||||
|
await Deno.readFile("./data/config.ini").then((e) =>
|
||||||
|
new TextDecoder().decode(e)
|
||||||
|
)
|
||||||
|
) || {};
|
||||||
|
|
||||||
|
export default config;
|
13
registry/src/db.ts
Normal file
13
registry/src/db.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Datastore } from "./deps.ts";
|
||||||
|
|
||||||
|
export interface IPackage {
|
||||||
|
name: string;
|
||||||
|
versions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new Datastore<IPackage>({
|
||||||
|
filename: "data/db.json",
|
||||||
|
autoload: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default db;
|
18
registry/src/deps.ts
Normal file
18
registry/src/deps.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// export { MongoClient, ObjectId } from "https://deno.land/x/mongo@v0.9.1/mod.ts";
|
||||||
|
|
||||||
|
export * as S3 from "https://deno.land/x/s3/mod.ts";
|
||||||
|
|
||||||
|
export * as Ini from "https://deno.land/x/ini/mod.ts";
|
||||||
|
|
||||||
|
export * as ABC from "https://deno.land/x/abc@v1/mod.ts";
|
||||||
|
export * as CorsMW from "https://deno.land/x/abc@v1/middleware/cors.ts";
|
||||||
|
export * as LoggerMW from "https://deno.land/x/abc@v1/middleware/logger.ts";
|
||||||
|
|
||||||
|
export * as Path from "https://deno.land/std@0.62.0/path/mod.ts";
|
||||||
|
export * as FS from "https://deno.land/std@0.62.0/fs/mod.ts";
|
||||||
|
|
||||||
|
export * as Compress from "https://git.stamm.me/Deno/DenReg/raw/branch/master/tar/mod.ts";
|
||||||
|
|
||||||
|
import DS from "https://raw.githubusercontent.com/hibas123/dndb/master/mod.ts";
|
||||||
|
|
||||||
|
export const Datastore = DS;
|
18
registry/src/http.ts
Normal file
18
registry/src/http.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ABC, CorsMW, LoggerMW } from "./deps.ts";
|
||||||
|
import config from "./config.ts";
|
||||||
|
|
||||||
|
const port = config?.api?.port || 8000;
|
||||||
|
const app = new ABC.Application();
|
||||||
|
|
||||||
|
app.use(LoggerMW.logger({}));
|
||||||
|
app.use(CorsMW.cors({}));
|
||||||
|
|
||||||
|
import api from "./http/api.ts";
|
||||||
|
api(app.group("api"));
|
||||||
|
|
||||||
|
import raw from "./http/raw.ts";
|
||||||
|
raw(app.group("raw"));
|
||||||
|
|
||||||
|
app.start({ port });
|
||||||
|
console.log("Running server at http://0.0.0.0:" + port);
|
||||||
|
console.log("Open at http://127.0.0.1:" + port);
|
133
registry/src/http/api.ts
Normal file
133
registry/src/http/api.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { ABC, Path, Compress, FS } 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) {
|
||||||
|
g.get("/", (ctx) => {
|
||||||
|
return { version: "1" };
|
||||||
|
});
|
||||||
|
|
||||||
|
g.post("/package/:name", uploadPackage, basicauth("api"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!isValidPackageName(packageName))
|
||||||
|
throw new Error("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) {
|
||||||
|
throw new Error("No version available in meta.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageVersion = meta.version;
|
||||||
|
|
||||||
|
console.log("Checking correct version");
|
||||||
|
|
||||||
|
if (!isValidFullVersion(packageVersion)) {
|
||||||
|
throw new Error("Invalid version. Version must be in format: 0.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Checking for previous uploads");
|
||||||
|
|
||||||
|
let packageMeta = await db.findOne({ name: packageName });
|
||||||
|
|
||||||
|
if (!packageMeta) {
|
||||||
|
packageMeta = {
|
||||||
|
name: packageName,
|
||||||
|
versions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.insert(packageMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Check if version was uploaded before");
|
||||||
|
|
||||||
|
if (packageMeta.versions.find((e) => e === meta.version)) {
|
||||||
|
throw new Error("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);
|
||||||
|
|
||||||
|
const bucketPath = (bucketBase + relative).replace(/@/g, "§");
|
||||||
|
|
||||||
|
console.log("Uploading file:", file.path, bucketPath, bucketBase);
|
||||||
|
await bucket.putObject(
|
||||||
|
bucketPath,
|
||||||
|
await Deno.readAll(await Deno.open(file.path)),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log("Setting new live version");
|
||||||
|
|
||||||
|
//TODO: Better option, since this could error whith multiple upload to the same package
|
||||||
|
await db.update(
|
||||||
|
{ name: packageName },
|
||||||
|
{
|
||||||
|
$set: { versions: [...packageMeta.versions, packageVersion] },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Finished successfully");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("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);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
53
registry/src/http/raw.ts
Normal file
53
registry/src/http/raw.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ABC } from "../deps.ts";
|
||||||
|
|
||||||
|
import { sortVersions, extractPackagePath } from "../utils.ts";
|
||||||
|
|
||||||
|
import db, { IPackage } from "../db.ts";
|
||||||
|
|
||||||
|
import bucket from "../s3.ts";
|
||||||
|
|
||||||
|
export default function raw(g: ABC.Group) {
|
||||||
|
g.get("/:package/*path", async (ctx) => {
|
||||||
|
console.log(ctx.params, ctx.path);
|
||||||
|
let [packageName, packageVersion] = extractPackagePath(
|
||||||
|
ctx.params.package
|
||||||
|
);
|
||||||
|
|
||||||
|
const meta = await db.findOne({ name: packageName });
|
||||||
|
|
||||||
|
console.log(packageName, await db.findOne({ name: packageName }));
|
||||||
|
|
||||||
|
const E404 = () => {
|
||||||
|
ctx.response.status = 404;
|
||||||
|
ctx.response.body = "Not found!";
|
||||||
|
throw new Error("Not found!");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!meta || meta.versions.length < 1) return E404();
|
||||||
|
|
||||||
|
const versions = meta.versions.sort(sortVersions).reverse();
|
||||||
|
|
||||||
|
if (!packageVersion) {
|
||||||
|
packageVersion = versions[0];
|
||||||
|
} else {
|
||||||
|
const v = versions.filter((e) =>
|
||||||
|
e.startsWith(packageVersion as string)
|
||||||
|
);
|
||||||
|
if (v.length < 1) return E404();
|
||||||
|
packageVersion = v[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucketPath = (
|
||||||
|
"packages/" +
|
||||||
|
packageName +
|
||||||
|
"/" +
|
||||||
|
packageVersion +
|
||||||
|
"/" +
|
||||||
|
ctx.params.path
|
||||||
|
).replace(/@/g, "§");
|
||||||
|
|
||||||
|
console.log("Getting file from:", bucketPath);
|
||||||
|
|
||||||
|
return (await bucket.getObject(bucketPath))?.body;
|
||||||
|
});
|
||||||
|
}
|
18
registry/src/registry.ts
Normal file
18
registry/src/registry.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const ensureDir = async (name: string) => {
|
||||||
|
if (
|
||||||
|
!(await Deno.stat(name)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false))
|
||||||
|
) {
|
||||||
|
await Deno.mkdir(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Deno.remove("./tmp", { recursive: true });
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
await ensureDir("./tmp");
|
||||||
|
await ensureDir("./data");
|
||||||
|
|
||||||
|
import "./http.ts";
|
16
registry/src/s3.ts
Normal file
16
registry/src/s3.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { S3 } from "./deps.ts";
|
||||||
|
import config from "./config.ts";
|
||||||
|
|
||||||
|
if (!config.s3) {
|
||||||
|
throw new Error("Config is missing [s3] section!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = new S3.S3Bucket({
|
||||||
|
bucket: config.s3.bucket || "deno-registry",
|
||||||
|
endpointURL: config.s3.endpoint,
|
||||||
|
accessKeyID: config.s3.accessKey,
|
||||||
|
secretKey: config.s3.secretKey,
|
||||||
|
region: config?.s3?.region || "us-east-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default bucket;
|
86
registry/src/utils.ts
Normal file
86
registry/src/utils.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { ABC } from "./deps.ts";
|
||||||
|
import { decode } from "https://deno.land/std/encoding/base64.ts";
|
||||||
|
import config from "./config.ts";
|
||||||
|
|
||||||
|
const packageNameRegex = /^[@]?[a-zA-Z][\d\w\-\_]*$/g.compile();
|
||||||
|
const packageVersionRegex = /^\d(\.\d)?(\.\d)?$/g.compile();
|
||||||
|
const packageFullVersionRegex = /^\d(\.\d)(\.\d)$/g.compile();
|
||||||
|
|
||||||
|
export const isValidPackageName = (name: string) => packageNameRegex.test(name);
|
||||||
|
export const isValidVersion = (version: string) =>
|
||||||
|
packageVersionRegex.test(version);
|
||||||
|
export const isValidFullVersion = (version: string) =>
|
||||||
|
packageFullVersionRegex.test(version);
|
||||||
|
|
||||||
|
const ALg = 1;
|
||||||
|
const ASm = -ALg;
|
||||||
|
const Equ = 0;
|
||||||
|
|
||||||
|
export const sortVersions = (a: string, b: string) => {
|
||||||
|
const [a1, a2, a3] = a.split(".").map(Number);
|
||||||
|
const [b1, b2, b3] = b.split(".").map(Number);
|
||||||
|
|
||||||
|
if (a1 > b1) return ALg;
|
||||||
|
if (a1 < b1) return ASm;
|
||||||
|
|
||||||
|
if (a2 > b2) return ALg;
|
||||||
|
if (a2 < b2) return ASm;
|
||||||
|
|
||||||
|
if (a3 > b3) return ALg;
|
||||||
|
if (a3 < b3) return ASm;
|
||||||
|
|
||||||
|
return Equ;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const basicauth = (realm: string) => (next: ABC.HandlerFunc) => (
|
||||||
|
ctx: ABC.Context
|
||||||
|
) => {
|
||||||
|
const value = ctx.request.headers.get("authorization");
|
||||||
|
|
||||||
|
console.log("Header:", value);
|
||||||
|
|
||||||
|
if (value && value.toLowerCase().startsWith("basic ")) {
|
||||||
|
const credentials = value.slice(6);
|
||||||
|
const [username, passwd] = new TextDecoder()
|
||||||
|
.decode(decode(credentials))
|
||||||
|
.split(":", 2);
|
||||||
|
|
||||||
|
if (config?.user[username]?.password === passwd) {
|
||||||
|
console.log("User authenticated!");
|
||||||
|
return next(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Authentication required");
|
||||||
|
ctx.response.status = 401;
|
||||||
|
ctx.response.headers.set("WWW-Authenticate", "Basic realm=" + realm);
|
||||||
|
return {
|
||||||
|
statusCode: 401,
|
||||||
|
error: "Authentication required",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function extractPackagePath(path: string): [string, string | undefined] {
|
||||||
|
let packageName = "";
|
||||||
|
if (path.startsWith("@")) {
|
||||||
|
packageName = "@";
|
||||||
|
path = path.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = path.split("@");
|
||||||
|
if (parts.length > 2) throw new Error("Invalid package name!");
|
||||||
|
|
||||||
|
packageName += parts[0];
|
||||||
|
let packageVersion: string | undefined = path[1];
|
||||||
|
|
||||||
|
if (!isValidPackageName(packageName))
|
||||||
|
throw new Error("Invalid package name!");
|
||||||
|
|
||||||
|
if (packageVersion !== "") {
|
||||||
|
if (!isValidVersion(packageVersion))
|
||||||
|
throw new Error("Invalid package version!");
|
||||||
|
else packageVersion = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [packageName, packageVersion];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user