Skip to content

Commit

Permalink
feat: support running on CF Workers
Browse files Browse the repository at this point in the history
Switches to limited number of node modules that Workers support.
Adds workarounds to mismatches between Deno and Node.

- [deps]: uses simpler check for `config_dir` to avoid `node:os`.
- [deps]: switches to `@sntran/html-rewriter` for cross runtime.
- [mod]: fallback for undefined `import.meta.url` in Workers.
- [backend/drive/auth]: move `reveal` out of global scope.
- [backend/local]: switch to dynamic import for `node:fs/promises`.
- [cmd/config]: switch to dynamic import for `node:fs/promises`.
- [cmd/lsf]: creates new `Headers` instead of spreading.
- [cmd/serve/http]: switches to `@sntran/serve` for cross runtime.

Added a `_worker.js` script to run as example on CF Workers.
  • Loading branch information
sntran committed Jun 29, 2024
1 parent f75e227 commit 8bc6ca2
Show file tree
Hide file tree
Showing 20 changed files with 2,132 additions and 169 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.env
node_modules
.DS_Store
.wrangler
76 changes: 76 additions & 0 deletions _worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as backends from "./backend/main.js";

/**
* A handler
*
* @callback Handler
* @param {Request} request
* @param {Object} env Environment variables.
* @returns {Response|Promise<Response>}
*/

/**
* Serves a bare remote, i.e., `/:memory:/path/to/file`
*
* All configuration must be passed as query parameters.
*
* @param {Request} request
* @param {Object} env
* @param {Object} params
* @param {string} params.remote
* @param {string} [params.path]
* @returns {Promise<Response}
*/
function remote(request, _env, params) {
let { remote, path = "/" } = params;

const backend = backends[remote];
if (!backend) {
return new Response("Remote not found.", { status: 404 });
}

if (!path) {
path = "/";
}

const url = new URL(request.url);
url.pathname = path;

request = new Request(url, request);

return backend.fetch(request);
}

const routes = {
"/\\::remote\\:/": remote,
"/\\::remote\\:/:path*": remote,
};

/**
* Routes request to the appropriate handler.
* @param {Object} routes
* @returns {Handler}
*/
function router(routes = {}) {
return function (request, env) {
const url = new URL(request.url);

for (const [route, handler] of Object.entries(routes)) {
const [pathname, method] = route.split("@").reverse();

if (method && request.method !== method) continue;

const pattern = new URLPattern({ pathname });
if (!pattern.test(url)) continue;

const params = pattern.exec(url)?.pathname?.groups || {};
return handler(request, env, params);
}

return env.ASSETS.fetch(request);
};
}

export default {
fetch: router(routes),
};
4 changes: 2 additions & 2 deletions backend/alias/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { fetch } from "../../mod.js";
* @param {Request} request
* @returns {Promise<Response>}
*/
function router(request) {
function alias(request) {
const { pathname, searchParams } = new URL(request.url);
const remote = searchParams.get("remote");

Expand All @@ -39,5 +39,5 @@ function router(request) {
}

export default {
fetch: router,
fetch: alias,
};
4 changes: 2 additions & 2 deletions backend/chunker/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const options = {
* @param {Request} request
* @returns {Promise<Response>}
*/
async function router(request) {
async function chunker(request) {
const { method, url } = request;
const { pathname, searchParams } = new URL(url);

Expand Down Expand Up @@ -385,5 +385,5 @@ async function upload(url, init) {
}

export default {
fetch: router,
fetch: chunker,
};
4 changes: 2 additions & 2 deletions backend/crypt/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ const SECRETBOX_OPTIONS = {
* @param {Request} request
* @returns {Promise<Response>}
*/
async function router(request) {
async function crypt(request) {
let { pathname, searchParams } = new URL(request.url);

let remote = searchParams.get("remote");
Expand Down Expand Up @@ -341,5 +341,5 @@ async function deriveKey(encPass, encSalt) {
}

export default {
fetch: router,
fetch: crypt,
};
10 changes: 6 additions & 4 deletions backend/drive/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { reveal } from "../../cmd/obscure/main.js";
const TOKEN_URL = "https://oauth2.googleapis.com/token";

const CLIENT_ID = "202264815644.apps.googleusercontent.com";
const CLIENT_SECRET = await reveal(
"eX8GpZTVx3vxMWVkuuBdDWmAUE6rGhTwVrvG9GhllYccSdj2-mvHVg",
).then((r) => r.text());
// This is actually an obscured value. Must use `reveal`.
const CLIENT_SECRET = "eX8GpZTVx3vxMWVkuuBdDWmAUE6rGhTwVrvG9GhllYccSdj2-mvHVg";
const SCOPE = "drive";

const encoder = new TextEncoder();
Expand Down Expand Up @@ -71,7 +70,10 @@ export async function auth(request) {
body.set("assertion", jwt);
} else {
const client_id = searchParams.get("client_id") || CLIENT_ID;
const client_secret = searchParams.get("client_secret") || CLIENT_SECRET;
let client_secret = searchParams.get("client_secret");
if (!client_secret) {
client_secret = await reveal(CLIENT_SECRET).then((r) => r.text());
}
let token = searchParams.get("token") || "";
try {
// Refresh token can be passed inside a JSON (rclone style) or the token itself.
Expand Down
4 changes: 2 additions & 2 deletions backend/drive/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const FILE_HEADERS = new Headers({
* @param {Request} request
* @returns {Promise<Response>}
*/
async function router(request) {
async function drive(request) {
//#region Auth
const response = await auth(request);
if (!response.ok) {
Expand Down Expand Up @@ -207,5 +207,5 @@ async function router(request) {
}

export default {
fetch: router,
fetch: drive,
};
28 changes: 17 additions & 11 deletions backend/fshare/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,12 @@ const APP_KEY = "dMnqMMZMUnN5YpvKENaEhdQQ5jxDqddt";
* @property {number} [modified]
*/

const authResponse = new Response("401 Unauthorized", {
status: 401,
statusText: "Unauthorized",
headers: {
"WWW-Authenticate": `Basic realm="Login", charset="UTF-8"`,
},
});

/**
* Serves an FShare remote
* @param {Request} request
* @returns {Promise<Response>}
*/
async function router(request) {
async function fshare(request) {
const { method, url } = request;
let { pathname, searchParams } = new URL(url);

Expand Down Expand Up @@ -298,6 +290,14 @@ async function authFetch(request, init) {
* @returns {Promise<Response>}
*/
export async function auth(request) {
const authResponse = new Response("401 Unauthorized", {
status: 401,
statusText: "Unauthorized",
headers: {
"WWW-Authenticate": `Basic realm="Login", charset="UTF-8"`,
},
});

const headers = request.headers;
const authorization = headers.get("Authorization");
if (!authorization) {
Expand Down Expand Up @@ -410,7 +410,13 @@ export async function download(config, url, init = {}) {
const { location } = await response.json();

if (!location) {
return authResponse;
return new Response("401 Unauthorized", {
status: 401,
statusText: "Unauthorized",
headers: {
"WWW-Authenticate": `Basic realm="Login", charset="UTF-8"`,
},
});
}

if (redirect === "manual") {
Expand All @@ -424,5 +430,5 @@ export async function download(config, url, init = {}) {
}

export default {
fetch: router,
fetch: fshare,
};
4 changes: 2 additions & 2 deletions backend/http/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const options = {
* @param {Request} request
* @returns {Promise<Response>}
*/
async function router(request) {
async function http(request) {
const { url, body } = request;
const { pathname, searchParams } = new URL(url);

Expand Down Expand Up @@ -218,5 +218,5 @@ async function router(request) {
}

export default {
fetch: router,
fetch: http,
};
33 changes: 18 additions & 15 deletions backend/local/main.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
#!/usr/bin/env -S deno serve --allow-all

import { cwd } from "node:process";
import { mkdir, open, readdir, rm, stat } from "node:fs/promises";
import process from "node:process";
import { contentType, extname, formatBytes, join } from "../../deps.js";

/**
* Serves a local remote
* @param {Request} request
* @returns {Promise<Response>}
*/
async function router(request) {
async function local(request) {
const { method, url } = request;
const { pathname, searchParams } = new URL(url);
const absolutePath = join(cwd(), pathname);
const absolutePath = join(process.cwd?.() || ".", pathname);

// Have to use dynamic import here because `fs/promises` is not available in
// Worker environments.
const { mkdir, open, readdir, rm, stat } = await import("node:fs/promises");

let stats;

Expand All @@ -23,7 +26,6 @@ async function router(request) {
stats = await stat(absolutePath);

if (stats.isDirectory()) {
// TODO: refactor to use `node:fs`
/**
* @type {import("node:fs").Dirent[]}
*/
Expand Down Expand Up @@ -70,7 +72,6 @@ async function router(request) {
filePath += "/";
}

// TODO: refactor to use `node:fs`
const { size, mtime } = await stat(
join(absolutePath, filePath),
);
Expand Down Expand Up @@ -145,14 +146,16 @@ async function router(request) {
await mkdir(absolutePath, { recursive: true });
} else {
const file = await open(absolutePath, "w");
await request.body.pipeTo(new WritableStream({
async write(chunk) {
await file.write(chunk);
},
async close() {
await file.close();
},
}));
await request.body.pipeTo(
new WritableStream({
async write(chunk) {
await file.write(chunk);
},
async close() {
await file.close();
},
}),
);
}

status = 201;
Expand All @@ -171,5 +174,5 @@ async function router(request) {
}

export default {
fetch: router,
fetch: local,
};
4 changes: 2 additions & 2 deletions backend/memory/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ cache.set("/", null);
* @param {Request} request
* @returns {Promise<Response>}
*/
async function router(request) {
async function memory(request) {
const { method, url } = request;
const { pathname, searchParams } = new URL(url);

Expand Down Expand Up @@ -189,5 +189,5 @@ async function router(request) {
}

export default {
fetch: router,
fetch: memory,
};
3 changes: 2 additions & 1 deletion cmd/config/main.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { env } from "node:process";
import { readFile } from "node:fs/promises";

import { config_dir, INI, join } from "../../deps.js";

Expand Down Expand Up @@ -39,6 +38,8 @@ const NAME_REGEX = /^[\w.][\w.\s-]*$/;
* @returns {Promise<Response>} The response.
*/
export async function config(subcommand, name, options, init) {
const { readFile } = await import("node:fs/promises");

let file = "", ini = "";
// Order as specified at https://rclone.org/docs/#config-config-file.
const PATHS = [
Expand Down
12 changes: 6 additions & 6 deletions cmd/lsf/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,16 @@ export async function lsf(location, flags = {}) {
} = flags;

const response = await lsjson(location, flags);
const { headers, ok } = response;
let { ok, headers, body } = response;

if (!ok) {
return response;
}

const body = response.body
headers = new Headers(response.headers);
headers.set("Content-Type", "text/plain");

body = body
.pipeThrough(new TextDecoderStream())
.pipeThrough(
new TransformStream({
Expand Down Expand Up @@ -169,9 +172,6 @@ export async function lsf(location, flags = {}) {
.pipeThrough(new TextEncoderStream());

return new Response(body, {
headers: {
...headers,
"Content-Type": "text/plain",
},
headers,
});
}
Loading

0 comments on commit 8bc6ca2

Please sign in to comment.