diff --git a/registry/src/db.ts b/registry/src/db.ts index 5e7caff..c7b49b3 100644 --- a/registry/src/db.ts +++ b/registry/src/db.ts @@ -4,12 +4,27 @@ await FS.ensureDir("./data"); export interface IPackage { name: string; + author: string; + description: string; versions: string[]; } -const db = new Datastore({ - filename: "data/db.json", - autoload: true, -}); +export interface IApiKey { + user: string; + key: string; + createdAt: Date; + lastAccess?: Date; + lastIP?: string; +} + +const db = { + package: new Datastore({ + filename: "data/packages.json", + autoload: true, + }), + api_key: new Datastore({ + filename: "data/api_keys.json", + }), +}; export default db; diff --git a/registry/src/deps.ts b/registry/src/deps.ts index 972a98d..829fc60 100644 --- a/registry/src/deps.ts +++ b/registry/src/deps.ts @@ -13,6 +13,8 @@ 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"; +export { Marked } from "https://deno.land/x/markdown/mod.ts"; + import DS from "https://raw.githubusercontent.com/hibas123/dndb/master/mod.ts"; /// @@ -20,6 +22,6 @@ export { React, jsx, Fragment, -} from "https://deno.hibas123.de/raw/@denreg-jsx/mod.ts"; +} from "https://raw.githubusercontent.com/apiel/jsx-html/master/mod.ts"; export const Datastore = DS; diff --git a/registry/src/http.ts b/registry/src/http.ts index 5caa4e4..f5904ef 100644 --- a/registry/src/http.ts +++ b/registry/src/http.ts @@ -9,13 +9,15 @@ app.use(LoggerMW.logger({})); app.use(CorsMW.cors({})); import api from "./http/api.ts"; -api(app.group("api")); +api(app.group("/api")); import raw from "./http/raw.ts"; -raw(app.group("raw")); +raw(app.group("/raw")); import view from "./http/views.ts"; -view(app.group("/")); +view(app); + +console.log(app.router.trees.GET); import render from "./renderer.tsx"; app.renderer = { diff --git a/registry/src/http/api.ts b/registry/src/http/api.ts index ded483a..c1cbb84 100644 --- a/registry/src/http/api.ts +++ b/registry/src/http/api.ts @@ -12,19 +12,40 @@ export default function api(g: ABC.Group) { return { version: "1" }; }); + // g.post("/getapikey", getApiKey, basicauth("api")); g.post("/package/:name", uploadPackage, 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; + const packageName = ctx.params.name.toLowerCase(); - if (!isValidPackageName(packageName)) - throw new Error("Invalid package name"); + if (!isValidPackageName(packageName)) { + return { + success: false, + message: "Invalid package name", + }; + } console.log("Writing body to tmp file:", filename); const file = await Deno.open(filename, { @@ -53,7 +74,10 @@ async function uploadPackage(ctx: ABC.Context) { console.log("Checking meta.json"); if (!meta?.version) { - throw new Error("No version available in meta.json"); + return { + success: false, + message: "No version available in meta.json", + }; } const packageVersion = meta.version; @@ -61,28 +85,42 @@ async function uploadPackage(ctx: ABC.Context) { console.log("Checking correct version"); if (!isValidFullVersion(packageVersion)) { - throw new Error("Invalid version. Version must be in format: 0.0.0"); + 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.findOne({ name: packageName }); + let packageMeta = await db.package.findOne({ name: packageName }); + + console.log(meta, packageMeta); if (!packageMeta) { packageMeta = { name: packageName, + author: meta.author, + description: meta.description, versions: [], }; - await db.insert(packageMeta); + await db.package.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!"); + return { + success: false, + message: "Version was already uploaded!", + }; } + packageMeta.author = meta.author; + packageMeta.description = meta.description; + const bucketBase = "packages/" + packageName + "/" + packageVersion + "/"; console.log("Uploading files to S3"); @@ -107,7 +145,7 @@ async function uploadPackage(ctx: ABC.Context) { console.log("Setting new live version"); //TODO: Better option, since this could error whith multiple upload to the same package - await db.update( + await db.package.update( { name: packageName }, { $set: { versions: [...packageMeta.versions, packageVersion] }, @@ -115,6 +153,9 @@ async function uploadPackage(ctx: ABC.Context) { ); console.log("Finished successfully"); + return { + success: true, + }; } catch (err) { console.error("Error while processing newly uploaded package"); console.error(err); @@ -127,7 +168,4 @@ async function uploadPackage(ctx: ABC.Context) { await Deno.remove(filename).catch(console.error); await Deno.remove(folder, { recursive: true }).catch(console.error); } - return { - success: true, - }; } diff --git a/registry/src/http/raw.ts b/registry/src/http/raw.ts index 0ef2226..18afae6 100644 --- a/registry/src/http/raw.ts +++ b/registry/src/http/raw.ts @@ -1,10 +1,5 @@ import { ABC } from "../deps.ts"; - -import { sortVersions, extractPackagePath } from "../utils.ts"; - -import db, { IPackage } from "../db.ts"; - -import bucket from "../s3.ts"; +import { extractPackagePath, getFile } from "../utils.ts"; export default function raw(g: ABC.Group) { g.get("/:package/*path", async (ctx) => { @@ -13,41 +8,17 @@ export default function raw(g: ABC.Group) { 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!"); + ctx.response.body = "// 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 + - "/" + + const result = await getFile( + packageName, + packageVersion, ctx.params.path - ).replace(/@/g, "§"); - - console.log("Getting file from:", bucketPath); - - return (await bucket.getObject(bucketPath))?.body; + ); + if (!result) return E404(); + return result; }); } diff --git a/registry/src/http/views.ts b/registry/src/http/views.ts index 45a2860..5f6f5fa 100644 --- a/registry/src/http/views.ts +++ b/registry/src/http/views.ts @@ -1,11 +1,13 @@ import { ABC } from "../deps.ts"; -import { basicauth } from "../utils.ts"; +import { basicauth, extractPackagePath } from "../utils.ts"; -export default function views(g: ABC.Group) { +export default function views(g: ABC.Application) { g.get( "/", async (ctx) => { - return ctx.render("index"); + return ctx.render("index", { + search: ctx.queryParams["q"], + }); // const render = await IndexView(); // console.log(render); // ctx.response.body = render; @@ -13,4 +15,15 @@ export default function views(g: ABC.Group) { }, basicauth("views") ); + g.get( + "/package/:package", + async (ctx) => { + let [packageName, packageVersion] = extractPackagePath( + ctx.params.package + ); + + return ctx.render("package", { packageName }); + }, + basicauth("views") + ); } diff --git a/registry/src/renderer.tsx b/registry/src/renderer.tsx index 190a714..4bbdac7 100644 --- a/registry/src/renderer.tsx +++ b/registry/src/renderer.tsx @@ -1,6 +1,8 @@ /// import { React, jsx } from "./deps.ts"; +import { v4 } from "https://deno.land/std/uuid/mod.ts"; + class StringReader implements Deno.Reader { private data: Uint8Array; private offset = 0; @@ -27,11 +29,12 @@ export default async function render( name: string, data: any ): Promise { + const id = v4.generate(); const component: { default: () => JSX.IntrinsicElements | Promise; - } = await import(`./views/${name}.tsx`); + } = await import(`./views/${name}.tsx?id=${id}.tsx`); const res = await ().render(); - console.log(res); + return new StringReader(res as string); } diff --git a/registry/src/utils.ts b/registry/src/utils.ts index 20f49c6..d9039d1 100644 --- a/registry/src/utils.ts +++ b/registry/src/utils.ts @@ -47,6 +47,8 @@ export const basicauth = (realm: string) => (next: ABC.HandlerFunc) => ( if (config?.user[username]?.password === passwd) { console.log("User authenticated!"); + if (!ctx.customContext) ctx.customContext = {}; + ctx.customContext.user = username; return next(ctx); } } @@ -62,6 +64,7 @@ export const basicauth = (realm: string) => (next: ABC.HandlerFunc) => ( export function extractPackagePath(path: string): [string, string | undefined] { let packageName = ""; + path = path.toLowerCase(); if (path.startsWith("@")) { packageName = "@"; path = path.slice(1); @@ -84,3 +87,48 @@ export function extractPackagePath(path: string): [string, string | undefined] { return [packageName, packageVersion]; } + +import db from "./db.ts"; + +import bucket from "./s3.ts"; +import { S3 } from "./deps.ts"; + +export async function getFile( + pkgName: string, + version: string | null | undefined, + file: string +): Promise { + const meta = await db.package.findOne({ name: pkgName }); + + if (!meta || meta.versions.length < 1) return null; + + const versions = meta.versions.sort(sortVersions).reverse(); + + if (!version) { + version = versions[0]; + } else { + const v = versions.filter((e) => e.startsWith(version as string)); + if (v.length < 1) return null; + version = v[0]; + } + + const bucketPath = ( + "packages/" + + pkgName + + "/" + + version + + "/" + + file + ).replace(/@/g, "§"); + + console.log("Getting file from:", bucketPath); + + try { + const data = (await bucket.getObject(bucketPath))?.body; + return data; + } catch (err) { + const msg = err.message as string; + if (msg.indexOf("404") >= 0) return null; + throw err; + } +} diff --git a/registry/src/views/_base.tsx b/registry/src/views/_base.tsx index 508c68c..9dd798d 100644 --- a/registry/src/views/_base.tsx +++ b/registry/src/views/_base.tsx @@ -1,16 +1,25 @@ /// import { React } from "../deps.ts"; +const styles = new TextDecoder().decode( + Deno.readFileSync("src/views/styles.css") +); + export default function Base(d: any, children: any[]) { return ( - */} + + - {children} + {children} ); } diff --git a/registry/src/views/_default.tsx b/registry/src/views/_default.tsx new file mode 100644 index 0000000..fa886c7 --- /dev/null +++ b/registry/src/views/_default.tsx @@ -0,0 +1,30 @@ +/// +import { React, Fragment } from "../deps.ts"; + +export function Main(a: any, children: any) { + return ( +
+
{children}
+
+ ); +} + +export function Menu({}: any, children: any) { + return ( +
+
+
+ + +
+ + + {children} +
+
+ ); +} diff --git a/registry/src/views/index.tsx b/registry/src/views/index.tsx index 3f632ac..c905425 100644 --- a/registry/src/views/index.tsx +++ b/registry/src/views/index.tsx @@ -2,32 +2,74 @@ import { React, Fragment } from "../deps.ts"; import Base from "./_base.tsx"; import DB, { IPackage } from "../db.ts"; +import { sortVersions } from "../utils.ts"; function Package({ pkg }: { pkg: IPackage }) { - const { name, versions } = pkg; + const { name, versions, author, description } = pkg; + + const sorted = versions.sort(sortVersions).reverse(); return ( -
-
{name}
-
    - {versions.map((version) => ( -
  • {version}
  • - ))} -
+
+
+

+ {name} {sorted[0]} +

+
By {author}
+
+ {/* {versions.map((version) => ( +
  • {version}
  • + ))} */} + {description} +
    +
    ); } -export default async function index() { - const packages = await DB.find({}); +import { Main, Menu } from "./_default.tsx"; + +export default async function index({ search }: any) { + let packages: IPackage[] = []; + if (search && search !== "") { + packages = await DB.package.find({ + name: RegExp(`${search}`), + }); + } else { + packages = await DB.package.find({}); + } return ( -
    +
    +
    +
    + {/* */} +
    + + +
    +
    +
    {packages.map((pkg) => ( ))} -
    + + +
      +
    • Item
    • +
    +
    ); } diff --git a/registry/src/views/package.tsx b/registry/src/views/package.tsx new file mode 100644 index 0000000..5ec8a07 --- /dev/null +++ b/registry/src/views/package.tsx @@ -0,0 +1,70 @@ +/// +import { React, Fragment, Marked } from "../deps.ts"; +import Base from "./_base.tsx"; +import DB, { IPackage } from "../db.ts"; +import { sortVersions, getFile } from "../utils.ts"; + +// function Package({ pkg }: { pkg: IPackage }) { +// const { name, versions, author } = pkg; + +// const sorted = versions.sort(sortVersions); + +// return ( +//
    +//
    +//

    {name}

    + +//
      +// {versions.map((version) => ( +//
    • {version}
    • +// ))} +// {author} {sorted[0]} +//
    +//
    +//
    +// ); +// } + +import { Main, Menu } from "./_default.tsx"; + +export default async function index({ packageName }: any) { + const pkg = await DB.package.findOne({ name: packageName }); + + if (!pkg) + return ( + +

    Not found

    + + ); + + const readmeContent = await getFile( + packageName, + undefined, + "README.md" + ).then((res) => { + if (res) + return Marked.parse(new TextDecoder().decode(res)).content as string; + else return undefined; + }); + + return ( + +
    +

    Package: {pkg.name}

    +

    + By {pkg.author} +

    + + {readmeContent !== undefined ? ( +
    + ) : ( +
    No README.md found!
    + )} +
    + + + ); +} diff --git a/registry/src/views/styles.css b/registry/src/views/styles.css new file mode 100644 index 0000000..4167020 --- /dev/null +++ b/registry/src/views/styles.css @@ -0,0 +1,17 @@ +.site { + display: grid; + grid-template-areas: "main menu"; + grid-template-columns: 2fr 1fr; + gap: 1rem; + padding-left: 1rem; + padding-right: 1rem; +} + +@media only screen and (max-width: 64rem) { + .site { + grid-template-columns: 1fr; + grid-template-areas: + "menu" + "main"; + } +}