+44
-31
proxy.js
+44
-31
proxy.js
···
27
27
const doc = await resolveDid(did);
28
28
const pds = doc.service[0].serviceEndpoint;
29
29
30
+
let record = await getRecord(pds, did, collection, rkey);
31
+
let updating = false;
32
+
33
+
async function updateRecord() {
34
+
if (updating) return;
35
+
try {
36
+
updating = true;
37
+
record = await getRecord(pds, did, collection, rkey);
38
+
} finally {
39
+
updating = false;
40
+
}
41
+
}
42
+
43
+
/**
44
+
*
45
+
* @param {number} status
46
+
* @param {string} message
47
+
*/
48
+
function error(res, status, message) {
49
+
res.statusCode = status;
50
+
res.end(message);
51
+
}
52
+
30
53
const server = createServer(async (req, res) => {
31
54
process.stdout.write(`${req.method} ${req.url} `);
32
-
if (req.method !== "GET") {
33
-
res.statusCode = 405;
34
-
res.end("Method not supported");
35
-
return;
36
-
}
55
+
const start = performance.now();
37
56
38
-
// TODO: keep leading slash
39
-
const path = req.url.slice(1);
57
+
try {
58
+
if (req.method !== "GET") return error(res, 405, "Method not supported");
59
+
queueMicrotask(updateRecord);
40
60
41
-
const record = await getRecord(pds, did, collection, rkey);
61
+
let asset = record.value.assets[req.url.slice(1)];
62
+
if (!asset) {
63
+
const path = req.url.slice(1).split("/"),
64
+
filename = path.pop();
65
+
if (!/^.+\..+$/.test(filename)) {
66
+
asset = record.value.assets[path.concat("index.html").join("/")];
67
+
}
68
+
}
42
69
43
-
for (const asset of record.value.assets) {
44
-
if (asset.path !== path) continue;
70
+
if (!asset) return error(res, 404, "Not found");
45
71
46
72
try {
47
-
const blob = await getBlob(pds, did, asset.file.ref.$link);
73
+
const blob = await getBlob(pds, did, asset.ref.$link);
48
74
49
-
if (!blob.ok) {
50
-
console.error(`Upstream error ${blob.status}: ${blob.statusText}`);
51
-
throw new Error("Response not ok");
52
-
}
53
-
54
-
if (!blob.body) {
55
-
console.error(`Blob body missing`);
56
-
throw new Error("Blob body missing");
57
-
}
75
+
if (!blob.ok) throw new Error(`Upstream error ${blob.status}: ${blob.statusText}`);
76
+
if (!blob.body) throw new Error("Blob body missing");
58
77
59
78
const contentType = blob.headers.get("content-type") || asset.file.mimeType;
60
79
res.setHeader("content-type", contentType);
···
65
84
if (done) break;
66
85
res.write(value);
67
86
}
68
-
69
87
res.end();
70
-
process.stdout.write(`${res.statusCode} \n`);
71
-
return;
72
88
} catch (error) {
73
-
res.statusCode = 502;
74
-
res.end("Error streaming blob");
75
-
process.stdout.write(`${res.statusCode} \n`);
76
-
return;
89
+
console.error(error);
90
+
return error(res, 502, "Bad gateway");
77
91
}
92
+
} finally {
93
+
const ms = performance.now() - start;
94
+
process.stdout.write(`${res.statusCode} - ${Math.round(ms)}ms\n`);
78
95
}
79
-
80
-
res.statusCode = 404;
81
-
res.end("Not Found");
82
-
process.stdout.write(`${res.statusCode} \n`);
83
96
});
84
97
85
98
const port = Number.parseInt(values.port) || 3000;
+47
-14
src/routes/~/sites/[name]/+page.svelte
+47
-14
src/routes/~/sites/[name]/+page.svelte
···
24
24
goto("/~/");
25
25
}
26
26
27
+
interface Bundle {
28
+
description?: string;
29
+
assets: Record<
30
+
string,
31
+
{ $type: "blob"; ref: { $link: string }; mimeType: string; size: number }
32
+
>;
33
+
createdAt: string;
34
+
}
35
+
27
36
async function deployBundle(e: SubmitEvent & { currentTarget: HTMLFormElement }) {
28
37
e.preventDefault();
29
38
const form = e.currentTarget;
···
36
45
if (typeof description !== "string" || !description) description = "Uploaded on website";
37
46
38
47
const files = formdata.getAll("files");
39
-
const assets: { path: string; file: any }[] = [];
48
+
const bundle: Bundle = { description, assets: {}, createdAt: new Date().toISOString() };
40
49
for (const file of files) {
41
50
if (!(file instanceof File)) continue;
42
51
43
52
const { data } = await rpc.post("com.atproto.repo.uploadBlob", { input: file });
44
53
if (isXRPCErrorPayload(data)) throw new Error("couldn't upload file");
45
54
46
-
assets.push({
47
-
path: file.name,
48
-
file: { $type: "blob", ref: data.blob.ref, mimeType: file.type, size: file.size },
49
-
});
55
+
const filepath = file.webkitRelativePath?.replace(/^.+\//, "") ?? file.name;
56
+
bundle.assets[filepath] = {
57
+
$type: "blob",
58
+
ref: data.blob.ref,
59
+
mimeType: file.type,
60
+
size: file.size,
61
+
};
50
62
}
51
63
52
-
const record = { description, assets, createdAt: new Date().toISOString() };
53
64
await rpc.post("com.atproto.repo.putRecord", {
54
-
input: { repo: data.did, collection: "com.jakelazaroff.test", rkey, record },
65
+
input: {
66
+
repo: data.did,
67
+
collection: "com.jakelazaroff.test",
68
+
rkey,
69
+
record: bundle as any,
70
+
},
55
71
});
56
72
if (isXRPCErrorPayload(data)) throw new Error("couldn't deploy");
57
73
58
74
invalidate(`rkey:${rkey}`);
75
+
form.reset();
59
76
}
60
77
</script>
61
78
···
69
86
<time>{data.record.value.createdAt}</time>
70
87
</summary>
71
88
<ul>
72
-
{#each data.record.value.assets as asset}
89
+
{#each Object.entries(data.record.value.assets) as [path, file]}
73
90
<li>
74
-
<span>{asset.path}</span>
91
+
<span>{path}</span>
75
92
<a
76
93
target="_blank"
77
-
href="{data.pds}xrpc/com.atproto.sync.getBlob?did={data.did}&cid={asset.file.ref.$link}"
78
-
>open</a
94
+
href="{data.pds}xrpc/com.atproto.sync.getBlob?did={data.did}&cid={file.ref.$link}">open</a
79
95
>
80
96
</li>
81
97
{/each}
82
98
</ul>
83
99
</details>
84
100
101
+
<h3>upload</h3>
85
102
<form onsubmit={deployBundle}>
86
103
<input type="hidden" name="rkey" value={params.name} />
87
-
<input name="description" />
88
-
<input type="file" name="files" />
104
+
<label>
105
+
<span>description</span>
106
+
<input name="description" />
107
+
</label>
108
+
<input type="file" name="files" webkitdirectory />
89
109
<button>upload</button>
90
110
</form>
91
111
112
+
<h3>settings</h3>
113
+
<form>
114
+
<label>
115
+
<span>not found</span>
116
+
<input name="notfound" />
117
+
</label>
118
+
<button>save</button>
119
+
</form>
120
+
121
+
<h3>danger zone</h3>
92
122
<form onsubmit={deleteBundle}>
93
-
<input name="rkey" pattern={params.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} />
123
+
<label>
124
+
<span>name</span>
125
+
<input name="rkey" pattern={params.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} />
126
+
</label>
94
127
<button>delete</button>
95
128
</form>
+1
-1
src/routes/~/sites/[name]/+page.ts
+1
-1
src/routes/~/sites/[name]/+page.ts
···
1
1
import type {} from "@atcute/atproto";
2
2
import { isXRPCErrorPayload } from "@atcute/client";
3
+
import { redirect } from "@sveltejs/kit";
3
4
4
5
import { client, configure } from "~/lib/oauth";
5
6
6
7
import type { PageLoad } from "./$types";
7
-
import { redirect } from "@sveltejs/kit";
8
8
9
9
configure();
10
10