samedche commited on
Commit
c938854
·
verified ·
1 Parent(s): e11694e

Update server.mjs

Browse files
Files changed (1) hide show
  1. server.mjs +96 -2
server.mjs CHANGED
@@ -11,6 +11,96 @@ const WORK = path.join(ROOT, "work");
11
  const jobs = new Map();
12
  const MAX_BYTES = 250 * 1024 * 1024;
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  await fs.mkdir(WORK, { recursive: true });
15
 
16
  function json(res, status, body) {
@@ -98,8 +188,12 @@ async function processJob(job) {
98
  const stat = await fs.stat(output);
99
  if (stat.size === 0) throw new Error("optimizer produced an empty file");
100
  job.outputBytes = stat.size;
 
 
 
 
101
  job.status = "ready";
102
- job.downloadPath = "/download/" + job.id;
103
  } catch (error) {
104
  job.status = "failed";
105
  job.error = error instanceof Error ? error.message : String(error);
@@ -113,7 +207,7 @@ const server = http.createServer(async (req, res) => {
113
  const url = new URL(req.url || "/", "http://" + (req.headers.host || "localhost"));
114
 
115
  if (req.method === "GET" && url.pathname === "/health") {
116
- return json(res, 200, { ok: true, service: "machesta-glb-optimizer", optimizer: "gltf-transform-draco" });
117
  }
118
 
119
  if (req.method === "POST" && url.pathname === "/optimize") {
 
11
  const jobs = new Map();
12
  const MAX_BYTES = 250 * 1024 * 1024;
13
 
14
+ const R2_ACCOUNT_ID = process.env.R2_ACCOUNT_ID || "";
15
+ const R2_BUCKET = process.env.R2_BUCKET || "";
16
+ const R2_ACCESS_KEY_ID = process.env.R2_ACCESS_KEY_ID || "";
17
+ const R2_SECRET_ACCESS_KEY = process.env.R2_SECRET_ACCESS_KEY || "";
18
+ const R2_PUBLIC_BASE_URL = process.env.R2_PUBLIC_BASE_URL || "";
19
+ const R2_REGION = "auto";
20
+ const R2_SERVICE = "s3";
21
+
22
+ function r2Configured() {
23
+ return Boolean(R2_ACCOUNT_ID && R2_BUCKET && R2_ACCESS_KEY_ID && R2_SECRET_ACCESS_KEY && R2_PUBLIC_BASE_URL);
24
+ }
25
+
26
+ function encodeRfc3986(value) {
27
+ return encodeURIComponent(value).replace(/[!'()*]/g, char => "%" + char.charCodeAt(0).toString(16).toUpperCase());
28
+ }
29
+
30
+ function encodePath(value) {
31
+ return value.split("/").map(encodeRfc3986).join("/");
32
+ }
33
+
34
+ function canonicalQueryString(params) {
35
+ return Object.keys(params).sort().map(key => encodeRfc3986(key) + "=" + encodeRfc3986(params[key])).join("&");
36
+ }
37
+
38
+ function sha256Hex(value) {
39
+ return crypto.createHash("sha256").update(value, "utf8").digest("hex");
40
+ }
41
+
42
+ function hmac(key, value) {
43
+ return crypto.createHmac("sha256", key).update(value, "utf8").digest();
44
+ }
45
+
46
+ function hmacHex(key, value) {
47
+ return crypto.createHmac("sha256", key).update(value, "utf8").digest("hex");
48
+ }
49
+
50
+ function getSignatureKey(secret, dateStamp) {
51
+ const kDate = hmac("AWS4" + secret, dateStamp);
52
+ const kRegion = hmac(kDate, R2_REGION);
53
+ const kService = hmac(kRegion, R2_SERVICE);
54
+ return hmac(kService, "aws4_request");
55
+ }
56
+
57
+ function toAmzDate(date) {
58
+ return date.toISOString().replace(/[:-]|\\.\\d{3}/g, "").slice(0, 15) + "Z";
59
+ }
60
+
61
+ function buildR2PresignedPutUrl(key, contentType) {
62
+ if (!r2Configured()) throw new Error("R2 secrets are not configured");
63
+ const now = new Date();
64
+ const host = R2_ACCOUNT_ID + ".r2.cloudflarestorage.com";
65
+ const amzDate = toAmzDate(now);
66
+ const dateStamp = amzDate.slice(0, 8);
67
+ const credentialScope = dateStamp + "/" + R2_REGION + "/" + R2_SERVICE + "/aws4_request";
68
+ const canonicalUri = "/" + encodePath(R2_BUCKET) + "/" + encodePath(key);
69
+ const signedHeaders = "content-type;host";
70
+ const query = {
71
+ "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
72
+ "X-Amz-Credential": R2_ACCESS_KEY_ID + "/" + credentialScope,
73
+ "X-Amz-Date": amzDate,
74
+ "X-Amz-Expires": "600",
75
+ "X-Amz-SignedHeaders": signedHeaders
76
+ };
77
+ const canonicalQuery = canonicalQueryString(query);
78
+ const canonicalHeaders = "content-type:" + contentType.trim().replace(/\\s+/g, " ") + "\\n" + "host:" + host + "\\n";
79
+ const canonicalRequest = ["PUT", canonicalUri, canonicalQuery, canonicalHeaders, signedHeaders, "UNSIGNED-PAYLOAD"].join("\\n");
80
+ const stringToSign = ["AWS4-HMAC-SHA256", amzDate, credentialScope, sha256Hex(canonicalRequest)].join("\\n");
81
+ const signature = hmacHex(getSignatureKey(R2_SECRET_ACCESS_KEY, dateStamp), stringToSign);
82
+ return "https://" + host + canonicalUri + "?" + canonicalQuery + "&X-Amz-Signature=" + signature;
83
+ }
84
+
85
+ function r2PublicUrl(key) {
86
+ return R2_PUBLIC_BASE_URL.replace(/\\/+$/, "") + "/" + encodePath(key);
87
+ }
88
+
89
+ async function uploadToR2(file, key) {
90
+ const contentType = "model/gltf-binary";
91
+ const data = await fs.readFile(file);
92
+ const uploadUrl = buildR2PresignedPutUrl(key, contentType);
93
+ const response = await fetch(uploadUrl, {
94
+ method: "PUT",
95
+ headers: { "content-type": contentType },
96
+ body: data,
97
+ signal: AbortSignal.timeout(180000)
98
+ });
99
+ if (!response.ok) throw new Error("R2 upload failed: HTTP " + response.status + " " + (await response.text()).slice(0, 1000));
100
+ return {key, url: r2PublicUrl(key), bytes: data.length};
101
+ }
102
+
103
+
104
  await fs.mkdir(WORK, { recursive: true });
105
 
106
  function json(res, status, body) {
 
188
  const stat = await fs.stat(output);
189
  if (stat.size === 0) throw new Error("optimizer produced an empty file");
190
  job.outputBytes = stat.size;
191
+ const r2Key = "optimized-glb/" + job.id + ".glb";
192
+ const uploaded = await uploadToR2(output, r2Key);
193
+ job.r2Key = uploaded.key;
194
+ job.r2Url = uploaded.url;
195
  job.status = "ready";
196
+ job.downloadPath = uploaded.url;
197
  } catch (error) {
198
  job.status = "failed";
199
  job.error = error instanceof Error ? error.message : String(error);
 
207
  const url = new URL(req.url || "/", "http://" + (req.headers.host || "localhost"));
208
 
209
  if (req.method === "GET" && url.pathname === "/health") {
210
+ return json(res, 200, { ok: true, service: "machesta-glb-optimizer", optimizer: "gltf-transform-draco", r2Configured: r2Configured() });
211
  }
212
 
213
  if (req.method === "POST" && url.pathname === "/optimize") {