Skip to content

Commit 6cc9a9c

Browse files
lukemeliaclaude
andcommitted
server: Add realm endpoint for writing binary files
Add a POST route for `application/octet-stream` that accepts binary uploads (images, etc.) and writes them through the existing `Realm.write()` pipeline. Widens write signatures across the adapter interface, node adapter, test adapter, and realm methods to accept `string | Uint8Array`. Applies the same size limit and response shape as `upsertCardSource`. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 877e98a commit 6cc9a9c

6 files changed

Lines changed: 288 additions & 37 deletions

File tree

packages/host/tests/helpers/adapter.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ interface Dir {
4848

4949
interface File {
5050
kind: 'file';
51-
content: string | object;
51+
content: string | object | Uint8Array;
5252
}
5353

5454
type CardAPI = typeof import('https://cardstack.com/base/card-api');
@@ -253,7 +253,7 @@ export class TestRealmAdapter implements RealmAdapter {
253253

254254
let value = content.content;
255255

256-
let fileRefContent = '';
256+
let fileRefContent: string | Uint8Array = '';
257257

258258
if (path.endsWith('.json')) {
259259
let cardApi = await this.#loader.import<CardAPI>(
@@ -272,6 +272,8 @@ export class TestRealmAdapter implements RealmAdapter {
272272
} else {
273273
fileRefContent = shimmedModuleIndicator;
274274
}
275+
} else if (value instanceof Uint8Array) {
276+
fileRefContent = value;
275277
} else {
276278
fileRefContent = value as string;
277279
}
@@ -291,7 +293,7 @@ export class TestRealmAdapter implements RealmAdapter {
291293

292294
async write(
293295
path: LocalPath,
294-
contents: string | object,
296+
contents: string | object | Uint8Array,
295297
): Promise<AdapterWriteResult> {
296298
let segments = path.split('/');
297299
let name = segments.pop()!;
@@ -326,9 +328,11 @@ export class TestRealmAdapter implements RealmAdapter {
326328
dir.contents[name] = {
327329
kind: 'file',
328330
content:
329-
typeof contents === 'string'
331+
contents instanceof Uint8Array
330332
? contents
331-
: JSON.stringify(contents, null, 2),
333+
: typeof contents === 'string'
334+
? contents
335+
: JSON.stringify(contents, null, 2),
332336
};
333337

334338
this.postUpdateEvent(updateEvent);

packages/realm-server/node-realm.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,10 @@ export class NodeAdapter implements RealmAdapter {
179179
};
180180
}
181181

182-
async write(path: string, contents: string): Promise<AdapterWriteResult> {
182+
async write(
183+
path: string,
184+
contents: string | Uint8Array,
185+
): Promise<AdapterWriteResult> {
183186
let absolutePath = join(this.realmDir, path);
184187
ensureFileSync(absolutePath);
185188
writeFileSync(absolutePath, contents);

packages/realm-server/tests/card-source-endpoints-test.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,204 @@ module(basename(__filename), function () {
10631063
});
10641064
});
10651065
});
1066+
1067+
module('binary file POST request', function (_hooks) {
1068+
module('public writable realm', function (hooks) {
1069+
setupPermissionedRealmAtURL(hooks, realmURL, {
1070+
permissions: {
1071+
'*': ['read', 'write'],
1072+
},
1073+
onRealmSetup,
1074+
});
1075+
1076+
let { getMessagesSince } = setupMatrixRoom(hooks, getRealmSetup);
1077+
1078+
test('serves a binary file POST request', async function (assert) {
1079+
let bytes = new Uint8Array([
1080+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe,
1081+
]);
1082+
let response = await request
1083+
.post('/test-image.png')
1084+
.set('Accept', 'application/octet-stream')
1085+
.set('Content-Type', 'application/octet-stream')
1086+
.send(Buffer.from(bytes));
1087+
1088+
assert.strictEqual(response.status, 204, 'HTTP 204 status');
1089+
assert.ok(
1090+
response.headers['x-created'],
1091+
'created date should be set for new binary file',
1092+
);
1093+
1094+
let filePath = join(
1095+
dir.name,
1096+
'realm_server_1',
1097+
'test',
1098+
'test-image.png',
1099+
);
1100+
assert.ok(existsSync(filePath), 'binary file exists on disk');
1101+
let fileBytes = readFileSync(filePath);
1102+
assert.deepEqual(
1103+
new Uint8Array(fileBytes),
1104+
bytes,
1105+
'file bytes match uploaded bytes',
1106+
);
1107+
});
1108+
1109+
test('creates file metadata for binary upload', async function (assert) {
1110+
let bytes = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
1111+
await request
1112+
.post('/meta-test.bin')
1113+
.set('Accept', 'application/octet-stream')
1114+
.set('Content-Type', 'application/octet-stream')
1115+
.send(Buffer.from(bytes));
1116+
1117+
let rows = await query(dbAdapter, [
1118+
'SELECT content_hash FROM realm_file_meta WHERE realm_url =',
1119+
param(testRealmHref),
1120+
'AND file_path =',
1121+
param('meta-test.bin'),
1122+
]);
1123+
assert.strictEqual(rows.length, 1, 'file meta row exists');
1124+
assert.ok(rows[0].content_hash, 'content hash is set');
1125+
});
1126+
1127+
test('overwrites existing binary file', async function (assert) {
1128+
let bytes1 = new Uint8Array([0x01, 0x02, 0x03]);
1129+
let bytes2 = new Uint8Array([0x04, 0x05, 0x06]);
1130+
1131+
let response1 = await request
1132+
.post('/overwrite-test.bin')
1133+
.set('Accept', 'application/octet-stream')
1134+
.set('Content-Type', 'application/octet-stream')
1135+
.send(Buffer.from(bytes1));
1136+
assert.strictEqual(
1137+
response1.status,
1138+
204,
1139+
'first upload returns 204',
1140+
);
1141+
1142+
let response2 = await request
1143+
.post('/overwrite-test.bin')
1144+
.set('Accept', 'application/octet-stream')
1145+
.set('Content-Type', 'application/octet-stream')
1146+
.send(Buffer.from(bytes2));
1147+
assert.strictEqual(
1148+
response2.status,
1149+
204,
1150+
'second upload returns 204',
1151+
);
1152+
1153+
let filePath = join(
1154+
dir.name,
1155+
'realm_server_1',
1156+
'test',
1157+
'overwrite-test.bin',
1158+
);
1159+
let fileBytes = readFileSync(filePath);
1160+
assert.deepEqual(
1161+
new Uint8Array(fileBytes),
1162+
bytes2,
1163+
'file contains second upload bytes',
1164+
);
1165+
});
1166+
1167+
test('broadcasts realm events for binary upload', async function (assert) {
1168+
let realmEventTimestampStart = Date.now();
1169+
1170+
await request
1171+
.post('/event-test.bin')
1172+
.set('Accept', 'application/octet-stream')
1173+
.set('Content-Type', 'application/octet-stream')
1174+
.send(Buffer.from(new Uint8Array([0xca, 0xfe])));
1175+
1176+
await expectIncrementalIndexEvent(
1177+
`${testRealmURL}event-test.bin`,
1178+
realmEventTimestampStart,
1179+
{
1180+
assert,
1181+
getMessagesSince,
1182+
realm: testRealmHref,
1183+
},
1184+
);
1185+
});
1186+
});
1187+
1188+
module(
1189+
'public writable realm with size limit for binary',
1190+
function (hooks) {
1191+
setupPermissionedRealmAtURL(hooks, realmURL, {
1192+
permissions: {
1193+
'*': ['read', 'write'],
1194+
},
1195+
cardSizeLimitBytes: 512,
1196+
onRealmSetup,
1197+
});
1198+
1199+
test('returns 413 when binary payload exceeds size limit', async function (assert) {
1200+
let oversized = new Uint8Array(2048).fill(0xff);
1201+
let response = await request
1202+
.post('/too-large.bin')
1203+
.set('Accept', 'application/octet-stream')
1204+
.set('Content-Type', 'application/octet-stream')
1205+
.send(Buffer.from(oversized));
1206+
1207+
assert.strictEqual(response.status, 413, 'HTTP 413 status');
1208+
assert.strictEqual(
1209+
response.body.errors[0].title,
1210+
'Payload Too Large',
1211+
'error title is correct',
1212+
);
1213+
});
1214+
},
1215+
);
1216+
1217+
module('permissioned realm for binary', function (hooks) {
1218+
setupPermissionedRealmAtURL(hooks, realmURL, {
1219+
permissions: {
1220+
john: ['read', 'write'],
1221+
},
1222+
onRealmSetup,
1223+
});
1224+
1225+
test('401 without a JWT for binary upload', async function (assert) {
1226+
let response = await request
1227+
.post('/secret.bin')
1228+
.set('Accept', 'application/octet-stream')
1229+
.set('Content-Type', 'application/octet-stream')
1230+
.send(Buffer.from(new Uint8Array([0x01])));
1231+
1232+
assert.strictEqual(response.status, 401, 'HTTP 401 status');
1233+
});
1234+
1235+
test('403 without permission for binary upload', async function (assert) {
1236+
let response = await request
1237+
.post('/secret.bin')
1238+
.set('Accept', 'application/octet-stream')
1239+
.set('Content-Type', 'application/octet-stream')
1240+
.send(Buffer.from(new Uint8Array([0x01])))
1241+
.set(
1242+
'Authorization',
1243+
`Bearer ${createJWT(testRealm, 'not-john')}`,
1244+
);
1245+
1246+
assert.strictEqual(response.status, 403, 'HTTP 403 status');
1247+
});
1248+
1249+
test('204 with permission for binary upload', async function (assert) {
1250+
let response = await request
1251+
.post('/secret.bin')
1252+
.set('Accept', 'application/octet-stream')
1253+
.set('Content-Type', 'application/octet-stream')
1254+
.send(Buffer.from(new Uint8Array([0x01])))
1255+
.set(
1256+
'Authorization',
1257+
`Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`,
1258+
);
1259+
1260+
assert.strictEqual(response.status, 204, 'HTTP 204 status');
1261+
});
1262+
});
1263+
});
10661264
});
10671265
});
10681266

0 commit comments

Comments
 (0)