feat(editor): a computer's files to and from the person's own files (F8.1)
Beside the dashboard only: Java's import window gets "Aus meinen Dateien" (D4) — the dashboard is asked over the bridge for a file of at most Java's 150 000 bytes and hands its bytes over — and a file's menu gets "In meine Dateien speichern" (D5), its bytes handed to the dashboard to save. - Bridge: chooseFile/chosenFile and saveFile/savedFile, each question with its number; an answer from elsewhere, of another version, for no question or of the other kind is no answer. - Whoever reads or finds the project frozen may save, not bring in. - doc fileBytes: a text as UTF-8, a picture as it is, and "missing" while a blob's bytes have not come (read so, the file would be empty). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import type { Id, VirtualFileContent, VirtualFileSystem } from '@mdg-filius/core';
|
||||
import { VirtualFileSystem as FileSystem } from '@mdg-filius/core';
|
||||
import { textBytes, VirtualFileSystem as FileSystem } from '@mdg-filius/core';
|
||||
import { blobSha, blobSideOf } from './blobs.js';
|
||||
import { fileEntry, fileSystemOf, inOrder, ROOTS } from './projectDoc.js';
|
||||
|
||||
@@ -125,6 +125,30 @@ export function fileText(doc: Y.Doc, id: Id, path: string): Y.Text | undefined {
|
||||
return entry?.get('text') as Y.Text | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A file's bytes to take out of the project (F8, D5: into the person's own
|
||||
* files): a text as UTF-8 — as the import reads it —, a picture or other
|
||||
* file as it is. `missing` while a blob's bytes have not come yet (they read
|
||||
* as none: saved so, the file would be empty); `notFile` for a folder or
|
||||
* nothing at the path.
|
||||
*/
|
||||
export function fileBytes(doc: Y.Doc, id: Id, path: string): { readonly name: string; readonly bytes: Uint8Array } | 'missing' | 'notFile' {
|
||||
const files = nodeMap(doc, id)?.get('files') as Y.Map<Y.Map<unknown>> | undefined;
|
||||
if (files === undefined) return 'notFile';
|
||||
const lower = path.toLowerCase();
|
||||
const found = files.has(path) ? ([path, files.get(path)!] as const) : [...files.entries()].find(([key]) => key.toLowerCase() === lower);
|
||||
if (found === undefined) return 'notFile';
|
||||
const [key, entry] = found;
|
||||
const name = key.slice(key.lastIndexOf('/') + 1);
|
||||
const kind = entry.get('kind');
|
||||
if (kind === 'text') return { name, bytes: textBytes((entry.get('text') as Y.Text).toString()) };
|
||||
if (kind !== 'binary' && kind !== 'image') return 'notFile';
|
||||
const blob = entry.get('blob');
|
||||
if (typeof blob !== 'string') return { name, bytes: entry.get('data') as Uint8Array };
|
||||
const held = blobSideOf(doc)?.bytes(blob);
|
||||
return held === undefined ? 'missing' : { name, bytes: held };
|
||||
}
|
||||
|
||||
/**
|
||||
* The document's files of a device into a file system (a running engine's):
|
||||
* what differs only, an entry the document no longer has removed, a new one
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
import { saveTextAs, VirtualFileSystem } from '@mdg-filius/core';
|
||||
import { addDevice, changeFiles, emptyProject, fileText, nodeFiles, readFilesInto, ROOTS, writeProject } from '../src/index.js';
|
||||
import { addDevice, attachBlobs, BLOB_THRESHOLD_BYTES, changeFiles, emptyProject, fileBytes, fileText, memoryBlobSide, nodeFiles, readFilesInto, ROOTS, writeProject } from '../src/index.js';
|
||||
|
||||
function setUp() {
|
||||
const doc = new Y.Doc();
|
||||
@@ -56,6 +56,35 @@ describe("a device's files in the document", () => {
|
||||
expect(fileText(empty, 'pc', '/Notiz.txt')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('give a file its bytes to take out: a text as UTF-8, a picture as it is, in any case; nothing for a folder or what is not there', () => {
|
||||
const { doc } = setUp();
|
||||
const picture = new Uint8Array([137, 80, 78, 71]);
|
||||
changeFiles(doc, 'pc', 'me', (fileSystem) => {
|
||||
fileSystem.writeTextFile('/Grüße.txt', 'Hallo Jörg\r\n');
|
||||
fileSystem.createDirectory('/ordner');
|
||||
fileSystem.writeImageFile('/ordner/Bild.png', picture, 'image/png');
|
||||
});
|
||||
expect(fileBytes(doc, 'pc', '/Grüße.txt')).toEqual({ name: 'Grüße.txt', bytes: new TextEncoder().encode('Hallo Jörg\r\n') });
|
||||
expect(fileBytes(doc, 'pc', '/ORDNER/bild.PNG')).toEqual({ name: 'Bild.png', bytes: picture });
|
||||
expect([fileBytes(doc, 'pc', '/ordner'), fileBytes(doc, 'pc', '/fehlt.txt'), fileBytes(doc, 'none', '/Grüße.txt')]).toEqual(['notFile', 'notFile', 'notFile']);
|
||||
});
|
||||
|
||||
it("say a blob is missing until its bytes have come — read so, the file would be empty", () => {
|
||||
const big = new Uint8Array(BLOB_THRESHOLD_BYTES + 5).map((_, index) => index % 251);
|
||||
const { doc } = setUp();
|
||||
attachBlobs(doc, memoryBlobSide());
|
||||
changeFiles(doc, 'pc', 'me', (fileSystem) => fileSystem.writeBinaryFile('/gross.bin', big));
|
||||
expect(fileBytes(doc, 'pc', '/gross.bin')).toEqual({ name: 'gross.bin', bytes: big });
|
||||
// Another browser: the document, but not the bytes yet.
|
||||
const there = new Y.Doc();
|
||||
const side = memoryBlobSide();
|
||||
attachBlobs(there, side);
|
||||
Y.applyUpdate(there, Y.encodeStateAsUpdate(doc));
|
||||
expect(fileBytes(there, 'pc', '/gross.bin')).toBe('missing');
|
||||
side.arrived(new Map([[(doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('files') as Y.Map<Y.Map<unknown>>).get('/gross.bin')!.get('blob') as string, big]]));
|
||||
expect(fileBytes(there, 'pc', '/gross.bin')).toEqual({ name: 'gross.bin', bytes: big });
|
||||
});
|
||||
|
||||
it("go into a running engine's file system, what differs only: new ones after the others, changed ones in their place, gone ones removed", () => {
|
||||
const { doc } = setUp();
|
||||
changeFiles(doc, 'pc', 'me', (fileSystem) => {
|
||||
|
||||
Reference in New Issue
Block a user