mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
129 lines
4.6 KiB
TypeScript
129 lines
4.6 KiB
TypeScript
import { File } from 'expo-file-system';
|
|
import { jmapClient } from './jmap-client';
|
|
import { secureFetch } from '../lib/client-cert';
|
|
|
|
export async function uploadBlob(
|
|
uri: string,
|
|
type: string,
|
|
): Promise<{ blobId: string; size: number; type: string }> {
|
|
const session = jmapClient.currentSession;
|
|
if (!session) throw new Error('Not connected');
|
|
|
|
const uploadUrl = session.uploadUrl.replace(
|
|
'{accountId}',
|
|
encodeURIComponent(jmapClient.accountId),
|
|
);
|
|
|
|
// Read via the file-system API so this works for both `file://` (image
|
|
// picker, iOS document picker) and `content://` (Android SAF) URIs. We pass
|
|
// the underlying ArrayBuffer rather than the Uint8Array view because some
|
|
// RN transports stringify typed-array bodies, which silently produces an
|
|
// empty/garbage upload.
|
|
const bytes = await new File(uri).bytes();
|
|
|
|
const response = await secureFetch(uploadUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': type || 'application/octet-stream',
|
|
Authorization: jmapClient.authHeader,
|
|
},
|
|
body: bytes.buffer as ArrayBuffer,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const detail = await response.text().catch(() => '');
|
|
throw new Error(`Upload failed: ${response.status}${detail ? ` ${detail}` : ''}`);
|
|
}
|
|
|
|
// JMAP servers return either the direct shape `{blobId, type, size}` or
|
|
// the per-account nested shape `{[accountId]: {blobId, type, size}}`.
|
|
// Stalwart has shipped both depending on version; mirror the webmail's
|
|
// tolerant parsing so uploads don't silently lose the blobId.
|
|
const raw = (await response.json()) as Record<string, unknown>;
|
|
const direct = raw as { blobId?: string; type?: string; size?: number };
|
|
if (typeof direct.blobId === 'string') {
|
|
return {
|
|
blobId: direct.blobId,
|
|
size: typeof direct.size === 'number' ? direct.size : bytes.byteLength,
|
|
type: typeof direct.type === 'string' && direct.type ? direct.type : type,
|
|
};
|
|
}
|
|
const nested = raw[jmapClient.accountId] as
|
|
| { blobId?: string; type?: string; size?: number }
|
|
| undefined;
|
|
if (nested?.blobId) {
|
|
return {
|
|
blobId: nested.blobId,
|
|
size: typeof nested.size === 'number' ? nested.size : bytes.byteLength,
|
|
type: typeof nested.type === 'string' && nested.type ? nested.type : type,
|
|
};
|
|
}
|
|
throw new Error('Upload succeeded but response did not include a blobId');
|
|
}
|
|
|
|
// Direct in-memory upload. The on-disk variant goes through expo-file-system's
|
|
// `new File(uri).bytes()`, which is overkill when we already have the bytes
|
|
// (e.g. the empty-blob trick used to create directories on Stalwart).
|
|
export async function uploadBytes(
|
|
bytes: Uint8Array,
|
|
type: string,
|
|
): Promise<{ blobId: string; size: number; type: string }> {
|
|
const session = jmapClient.currentSession;
|
|
if (!session) throw new Error('Not connected');
|
|
|
|
const uploadUrl = session.uploadUrl.replace(
|
|
'{accountId}',
|
|
encodeURIComponent(jmapClient.accountId),
|
|
);
|
|
|
|
const response = await secureFetch(uploadUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': type || 'application/octet-stream',
|
|
Authorization: jmapClient.authHeader,
|
|
},
|
|
body: bytes.buffer as ArrayBuffer,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const detail = await response.text().catch(() => '');
|
|
throw new Error(`Upload failed: ${response.status}${detail ? ` ${detail}` : ''}`);
|
|
}
|
|
|
|
const raw = (await response.json()) as Record<string, unknown>;
|
|
const direct = raw as { blobId?: string; type?: string; size?: number };
|
|
if (typeof direct.blobId === 'string') {
|
|
return {
|
|
blobId: direct.blobId,
|
|
size: typeof direct.size === 'number' ? direct.size : bytes.byteLength,
|
|
type: typeof direct.type === 'string' && direct.type ? direct.type : type,
|
|
};
|
|
}
|
|
const nested = raw[jmapClient.accountId] as
|
|
| { blobId?: string; type?: string; size?: number }
|
|
| undefined;
|
|
if (nested?.blobId) {
|
|
return {
|
|
blobId: nested.blobId,
|
|
size: typeof nested.size === 'number' ? nested.size : bytes.byteLength,
|
|
type: typeof nested.type === 'string' && nested.type ? nested.type : type,
|
|
};
|
|
}
|
|
throw new Error('Upload succeeded but response did not include a blobId');
|
|
}
|
|
|
|
export function getDownloadUrl(
|
|
blobId: string,
|
|
name?: string,
|
|
type?: string,
|
|
): string {
|
|
const session = jmapClient.currentSession;
|
|
if (!session) throw new Error('Not connected');
|
|
|
|
return session.downloadUrl
|
|
.replace('{accountId}', encodeURIComponent(jmapClient.accountId))
|
|
.replace('{blobId}', encodeURIComponent(blobId))
|
|
.replace('{name}', encodeURIComponent(name ?? 'download'))
|
|
.replace('{type}', encodeURIComponent(type ?? 'application/octet-stream'));
|
|
}
|