Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ To host your own instance of the proxy:
- Add your GitHub account to Pages.
- Grant access to your fork of `twilligon/git-lfs-s3-proxy`.
- Set up your Pages site: set **Build command** to `npm install` and leave all other settings on their defaults.
- The checked-in `wrangler.toml` intentionally does not set `limits.cpu_ms`, because Cloudflare only supports custom CPU limits on paid Workers plans. If you later add `[limits] cpu_ms = ...`, deployments from a Free plan account will fail.
- For slow or very large LFS pushes, you can set a Worker environment variable named `EXPIRY` to increase the presigned URL lifetime in seconds. For example, `86400` keeps each upload URL valid for 24 hours instead of the default 3600 seconds.
- If you own a domain name (e.g. `example.com`), you can [add a CNAME record](https://developers.cloudflare.com/pages/platform/custom-domains/#add-a-custom-cname-record) to point a subdomain (e.g. `git-lfs-s3-proxy.example.com`) at your instance. If you don't own a domain, a `pages.dev` subdomain will work just as well, except you'll have to change your LFS server URL if you ever stop using the proxy.

### Find your LFS server URL
Expand Down Expand Up @@ -181,4 +183,3 @@ Hopefully `aws4fetch` merges the [fix](https://github.com/mhart/aws4fetch/pull/7
For example, with a Linode bucket `my-repo` in `us-east-1` region with access key ID `foo` and secret access key `bar` via the default instance:

https://foo:bar@git-lfs-s3-proxy.pages.dev/service=s3/us-east-1.linodeobjects.com/my-repo

19 changes: 14 additions & 5 deletions _worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ const METHOD_FOR = {
download: "GET",
};

async function sign(s3, bucket, path, method) {
function parseExpiry(value) {
const expiry = Number.parseInt(value, 10);
if (!Number.isFinite(expiry) || expiry < 1) {
return EXPIRY;
}

// R2 presigned URLs support expiries up to 7 days.
return Math.min(expiry, 604800);
}

async function sign(s3, bucket, path, method, expiry) {
const info = { method };
const signed = await s3.sign(
new Request(`https://${bucket}/${path}?X-Amz-Expires=${EXPIRY}`, info),
new Request(`https://${bucket}/${path}?X-Amz-Expires=${expiry}`, info),
{ aws: { signQuery: true } },
);
return signed.url;
Expand Down Expand Up @@ -69,7 +79,6 @@ async function fetch(req, env) {
let s3Options = { accessKeyId: user, secretAccessKey: pass };

const segments = url.pathname.split("/").slice(1, -2);
let params = {};
let bucketIdx = 0;
for (const segment of segments) {
const sliceIdx = segment.indexOf("=");
Expand All @@ -86,7 +95,7 @@ async function fetch(req, env) {

const s3 = new AwsClient(s3Options);
const bucket = segments.slice(bucketIdx).join("/");
const expires_in = params.expiry || env.EXPIRY || EXPIRY;
const expires_in = parseExpiry(env.EXPIRY);

const { objects, operation, hash_algo = "sha256" } = await req.json();

Expand All @@ -113,7 +122,7 @@ async function fetch(req, env) {
authenticated: true,
actions: {
[operation]: {
href: await sign(s3, bucket, oid, method),
href: await sign(s3, bucket, oid, method, expires_in),
expires_in,
},
},
Expand Down
21 changes: 21 additions & 0 deletions wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name = "git-lfs-s3-proxy"
main = "./_worker.js"
compatibility_date = "2026-03-02"

[limits]
cpu_ms = 300_000

[observability]
enabled = false
head_sampling_rate = 1

[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true

[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1