Skip to content

Commit c1b01d1

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 c1b01d1

6 files changed

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

0 commit comments

Comments
 (0)