@@ -1,33 +0,0 @@
|
||||
import { assertEquals } from "../../test_deps.ts";
|
||||
import {
|
||||
decryptAES,
|
||||
decryptPath,
|
||||
deriveKeyFromPassword,
|
||||
encryptAES,
|
||||
encryptPath,
|
||||
} from "./aes.ts";
|
||||
|
||||
Deno.test("AES encryption and decryption", async () => {
|
||||
const password = "YourPassword";
|
||||
const salt = "UniquePerUserSalt";
|
||||
const message = "Hello, World!";
|
||||
|
||||
const key = await deriveKeyFromPassword(password, salt);
|
||||
const encrypted = await encryptAES(key, message);
|
||||
|
||||
const decrypted = await decryptAES(key, encrypted);
|
||||
assertEquals(decrypted, message);
|
||||
|
||||
// Test that checks if a path is encrypted the same way every time and can be unencrypted
|
||||
const path =
|
||||
"this/is/a/long/path/that/needs/to/be/encrypted because that's what we do.md";
|
||||
const encryptedPath = await encryptPath(key, path);
|
||||
const encryptedPath2 = await encryptPath(key, path);
|
||||
// Assure two runs give the same result
|
||||
assertEquals(encryptedPath, encryptedPath2);
|
||||
|
||||
// Ensure decryption works
|
||||
const decryptedPath = await decryptPath(key, encryptedPath);
|
||||
console.log(encryptedPath);
|
||||
assertEquals(decryptedPath, path);
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
import {
|
||||
base64Decode,
|
||||
base64Encode,
|
||||
} from "../../plugos/asset_bundle/base64.ts";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export async function deriveKeyFromPassword(
|
||||
password: string,
|
||||
salt: string,
|
||||
): Promise<CryptoKey> {
|
||||
const baseKey = encoder.encode(password);
|
||||
const importedKey = await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
baseKey,
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveKey"],
|
||||
);
|
||||
return crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: encoder.encode(salt),
|
||||
iterations: 10000,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
importedKey,
|
||||
{
|
||||
name: "AES-GCM",
|
||||
length: 256,
|
||||
},
|
||||
true,
|
||||
["encrypt", "decrypt"],
|
||||
);
|
||||
}
|
||||
|
||||
export async function encryptAES(
|
||||
key: CryptoKey,
|
||||
message: string,
|
||||
): Promise<ArrayBuffer> {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encodedMessage = encoder.encode(message);
|
||||
const ciphertext = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
encodedMessage,
|
||||
);
|
||||
return appendBuffer(iv, ciphertext);
|
||||
}
|
||||
|
||||
export async function decryptAES(
|
||||
key: CryptoKey,
|
||||
data: ArrayBuffer,
|
||||
): Promise<string> {
|
||||
const iv = data.slice(0, 12);
|
||||
const ciphertext = data.slice(12);
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
ciphertext,
|
||||
);
|
||||
return decoder.decode(decrypted);
|
||||
}
|
||||
|
||||
function appendBuffer(buffer1: ArrayBuffer, buffer2: ArrayBuffer): ArrayBuffer {
|
||||
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
|
||||
tmp.set(new Uint8Array(buffer1), 0);
|
||||
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
|
||||
return tmp.buffer;
|
||||
}
|
||||
|
||||
// This is against security recommendations, but we need a way to always generate the same encrypted path for the same path and password
|
||||
const pathIv = new Uint8Array(12); // 12 bytes of 0
|
||||
|
||||
export async function encryptPath(
|
||||
key: CryptoKey,
|
||||
path: string,
|
||||
): Promise<string> {
|
||||
const encodedMessage = encoder.encode(path);
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: pathIv,
|
||||
},
|
||||
key,
|
||||
encodedMessage,
|
||||
);
|
||||
return base64Encode(new Uint8Array(ciphertext));
|
||||
}
|
||||
|
||||
export async function decryptPath(
|
||||
key: CryptoKey,
|
||||
data: string,
|
||||
): Promise<string> {
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: pathIv,
|
||||
},
|
||||
key,
|
||||
base64Decode(data),
|
||||
);
|
||||
return decoder.decode(decrypted);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { MemoryKvPrimitives } from "../../plugos/lib/memory_kv_primitives.ts";
|
||||
import { assert, assertEquals } from "../../test_deps.ts";
|
||||
import { ChunkedKvStoreSpacePrimitives } from "./chunked_datastore_space_primitives.ts";
|
||||
import { EncryptedSpacePrimitives } from "./encrypted_space_primitives.ts";
|
||||
import { testSpacePrimitives } from "./space_primitives.test.ts";
|
||||
|
||||
Deno.test("Encrypted Space Primitives", async () => {
|
||||
// Using an in-memory store for testing
|
||||
const memoryKv = new MemoryKvPrimitives();
|
||||
const spacePrimitives = new EncryptedSpacePrimitives(
|
||||
new ChunkedKvStoreSpacePrimitives(
|
||||
memoryKv,
|
||||
1024 * 1024,
|
||||
),
|
||||
);
|
||||
assertEquals(false, await spacePrimitives.init());
|
||||
await spacePrimitives.setup("password");
|
||||
assertEquals(await spacePrimitives.fetchFileList(), []);
|
||||
await testSpacePrimitives(spacePrimitives);
|
||||
|
||||
// Let's try an incorrect password
|
||||
try {
|
||||
await spacePrimitives.login("wronk");
|
||||
assert(false);
|
||||
} catch (e: any) {
|
||||
assertEquals(e.message, "Incorrect password");
|
||||
}
|
||||
|
||||
// Now let's update the password
|
||||
await spacePrimitives.updatePassword("password", "password2");
|
||||
|
||||
try {
|
||||
await spacePrimitives.updatePassword("password", "password2");
|
||||
assert(false);
|
||||
} catch (e: any) {
|
||||
assertEquals(e.message, "Incorrect password");
|
||||
}
|
||||
|
||||
await spacePrimitives.writeFile(
|
||||
"test.txt",
|
||||
new TextEncoder().encode("Hello World"),
|
||||
);
|
||||
|
||||
// Let's do this again with the new password
|
||||
|
||||
const spacePrimitives2 = new EncryptedSpacePrimitives(
|
||||
new ChunkedKvStoreSpacePrimitives(
|
||||
memoryKv,
|
||||
1024 * 1024,
|
||||
),
|
||||
);
|
||||
assertEquals(true, await spacePrimitives2.init());
|
||||
await spacePrimitives2.login("password2");
|
||||
assertEquals(
|
||||
new TextDecoder().decode(
|
||||
(await spacePrimitives2.readFile("test.txt")).data,
|
||||
),
|
||||
"Hello World",
|
||||
);
|
||||
await spacePrimitives2.deleteFile("test.txt");
|
||||
await testSpacePrimitives(spacePrimitives2);
|
||||
});
|
||||
@@ -0,0 +1,455 @@
|
||||
import { FileMeta } from "../../plug-api/types.ts";
|
||||
import { SpacePrimitives } from "./space_primitives.ts";
|
||||
|
||||
export const encryptedFileExt = ".crypt";
|
||||
export const keyPath = "KEY";
|
||||
export const saltFile = "salt.crypt";
|
||||
|
||||
/**
|
||||
* This class adds an (AES) based encryption layer on top of another SpacePrimitives implementation.
|
||||
* It encrypts all file names and file contents.
|
||||
* It uses a key file (default named _KEY) to store the encryption key, this file is encrypted with a key derived from the user's password.
|
||||
* The reason to keep the actualy encryption key in a file is to allow the user to change their password without having to re-encrypt all files.
|
||||
* Important note: FileMeta's size will reflect the underlying encrypted size, not the original size
|
||||
*/
|
||||
export class EncryptedSpacePrimitives implements SpacePrimitives {
|
||||
private masterKey?: CryptoKey;
|
||||
private encryptedKeyFileName?: string;
|
||||
spaceSalt?: Uint8Array;
|
||||
|
||||
constructor(
|
||||
private wrapped: SpacePrimitives,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the space is initialized by loading the salt file.
|
||||
* @returns true if the space was initialized, false if it was not initialized yet
|
||||
*/
|
||||
async init(salt?: Uint8Array | undefined | null): Promise<boolean> {
|
||||
if (salt) {
|
||||
this.spaceSalt = salt;
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
this.spaceSalt = (await this.wrapped.readFile(saltFile)).data;
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
if (e.message === "Not found") {
|
||||
console.warn("Space not initialized");
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a fresh space with a new salt and master encryption key derived from a password
|
||||
* @param password
|
||||
*/
|
||||
async setup(password: string): Promise<void> {
|
||||
if (this.spaceSalt) {
|
||||
throw new Error("Space already initialized");
|
||||
}
|
||||
this.spaceSalt = this.generateSalt();
|
||||
await this.wrapped.writeFile(saltFile, this.spaceSalt);
|
||||
await this.createKey(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the encryption key from the master key based on the user's password
|
||||
* @param password the user's password
|
||||
*/
|
||||
async login(password: string): Promise<void> {
|
||||
if (!this.spaceSalt) {
|
||||
throw new Error("Space not initialized");
|
||||
}
|
||||
// First derive an encryption key solely used for encrypting the key file from the user's password
|
||||
const keyEncryptionKey = await this.deriveKeyFromPassword(password);
|
||||
const encryptedKeyFileName = await this.encryptPath(
|
||||
keyEncryptionKey,
|
||||
keyPath,
|
||||
);
|
||||
|
||||
try {
|
||||
this.masterKey = await this.importKey(
|
||||
await this.decryptAES(
|
||||
keyEncryptionKey,
|
||||
(await this.wrapped.readFile(
|
||||
encryptedKeyFileName,
|
||||
)).data,
|
||||
),
|
||||
);
|
||||
this.encryptedKeyFileName = encryptedKeyFileName;
|
||||
} catch (e: any) {
|
||||
if (e.message === "Not found") {
|
||||
throw new Error("Incorrect password");
|
||||
}
|
||||
console.trace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private generateKey(): Promise<CryptoKey> {
|
||||
return window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
length: 256,
|
||||
},
|
||||
true,
|
||||
["encrypt", "decrypt"],
|
||||
);
|
||||
}
|
||||
|
||||
private async createKey(password: string): Promise<void> {
|
||||
const keyEncryptionKey = await this.deriveKeyFromPassword(password);
|
||||
this.encryptedKeyFileName = await this.encryptPath(
|
||||
keyEncryptionKey,
|
||||
keyPath,
|
||||
);
|
||||
this.masterKey = await this.generateKey();
|
||||
// And write it
|
||||
await this.wrapped.writeFile(
|
||||
this.encryptedKeyFileName,
|
||||
await this.encryptAES(
|
||||
keyEncryptionKey,
|
||||
await this.exportKey(this.masterKey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async updatePassword(oldPassword: string, newPasword: string): Promise<void> {
|
||||
if (!this.masterKey) {
|
||||
throw new Error("No key loaded");
|
||||
}
|
||||
const oldPasswordKeyFileName = await this.encryptPath(
|
||||
await this.deriveKeyFromPassword(oldPassword),
|
||||
keyPath,
|
||||
);
|
||||
|
||||
// Check if the old password is correct
|
||||
try {
|
||||
await this.wrapped.getFileMeta(oldPasswordKeyFileName);
|
||||
} catch (e: any) {
|
||||
if (e.message === "Not found") {
|
||||
throw new Error("Incorrect password");
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// First derive an encryption key solely used for encrypting the key file from the user's password
|
||||
const keyEncryptionKey = await this.deriveKeyFromPassword(newPasword);
|
||||
|
||||
this.encryptedKeyFileName = await this.encryptPath(
|
||||
keyEncryptionKey,
|
||||
keyPath,
|
||||
);
|
||||
// And write it
|
||||
await this.wrapped.writeFile(
|
||||
this.encryptedKeyFileName,
|
||||
await this.encryptAES(
|
||||
keyEncryptionKey,
|
||||
await this.exportKey(this.masterKey),
|
||||
),
|
||||
);
|
||||
|
||||
// Then delete the old key file based on the old password
|
||||
await this.wrapped.deleteFile(oldPasswordKeyFileName);
|
||||
}
|
||||
|
||||
isUnencryptedPath(name: string) {
|
||||
return name.startsWith("_plug/");
|
||||
}
|
||||
|
||||
private generateSalt(): Uint8Array {
|
||||
return crypto.getRandomValues(new Uint8Array(16));
|
||||
}
|
||||
|
||||
private async exportKey(key: CryptoKey): Promise<Uint8Array> {
|
||||
const arrayBuffer = await window.crypto.subtle.exportKey("raw", key);
|
||||
return new Uint8Array(arrayBuffer);
|
||||
}
|
||||
|
||||
private importKey(key: Uint8Array): Promise<CryptoKey> {
|
||||
return window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
key,
|
||||
{ name: "AES-GCM" },
|
||||
true,
|
||||
["encrypt", "decrypt"],
|
||||
);
|
||||
}
|
||||
|
||||
private async deriveKeyFromPassword(
|
||||
password: string,
|
||||
): Promise<CryptoKey> {
|
||||
const baseKey = new TextEncoder().encode(password);
|
||||
const importedKey = await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
baseKey,
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveKey"],
|
||||
);
|
||||
return crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: this.spaceSalt!,
|
||||
iterations: 10000,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
importedKey,
|
||||
{
|
||||
name: "AES-GCM",
|
||||
length: 256,
|
||||
},
|
||||
true,
|
||||
["encrypt", "decrypt"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts using AES-GCM and prepends the IV to the ciphertext
|
||||
* @param key
|
||||
* @param message
|
||||
* @returns
|
||||
*/
|
||||
private async encryptAES(
|
||||
key: CryptoKey,
|
||||
message: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const ciphertext = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
message,
|
||||
);
|
||||
return appendBuffer(iv, new Uint8Array(ciphertext));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts using AES-GCM and expects the IV to be prepended to the ciphertext
|
||||
* @param key
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
async decryptAES(
|
||||
key: CryptoKey,
|
||||
data: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
const iv = data.slice(0, 12);
|
||||
const ciphertext = data.slice(12);
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
ciphertext,
|
||||
);
|
||||
return new Uint8Array(decrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Left pads a string with zeros to a length of 32, encrypts it using AES-GCM and returns the base32 encoded ciphertext
|
||||
* @param key
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
async encryptPath(
|
||||
key: CryptoKey,
|
||||
path: string,
|
||||
): Promise<string> {
|
||||
if (!this.spaceSalt) {
|
||||
throw new Error("Space not initialized");
|
||||
}
|
||||
if (this.isUnencryptedPath(path)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
path = path.padEnd(32, "\0");
|
||||
const encodedMessage = new TextEncoder().encode(path);
|
||||
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: this.spaceSalt,
|
||||
},
|
||||
key,
|
||||
encodedMessage,
|
||||
);
|
||||
const encodedPath = base32Encode(new Uint8Array(ciphertext));
|
||||
// console.log(new TextDecoder().decode(ciphertext));
|
||||
return encodedPath.slice(0, 3) + "/" + encodedPath.slice(3) +
|
||||
encryptedFileExt;
|
||||
}
|
||||
|
||||
private async decryptPath(
|
||||
key: CryptoKey,
|
||||
encryptedPath: string,
|
||||
): Promise<string> {
|
||||
if (!this.spaceSalt) {
|
||||
throw new Error("Space not initialized");
|
||||
}
|
||||
if (this.isUnencryptedPath(encryptedPath)) {
|
||||
return encryptedPath;
|
||||
}
|
||||
|
||||
if (!encryptedPath.endsWith(encryptedFileExt)) {
|
||||
throw new Error("Invalid encrypted path");
|
||||
}
|
||||
// Remove the extension and slashes
|
||||
encryptedPath = encryptedPath.slice(0, -encryptedFileExt.length).replaceAll(
|
||||
"/",
|
||||
"",
|
||||
);
|
||||
|
||||
// console.log("To decrypt", encryptedPath);
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: this.spaceSalt,
|
||||
},
|
||||
key,
|
||||
base32Decode(encryptedPath),
|
||||
);
|
||||
// Decode the buffer and remove the padding
|
||||
return removePadding(new TextDecoder().decode(decrypted), "\0");
|
||||
}
|
||||
|
||||
async fetchFileList(): Promise<FileMeta[]> {
|
||||
const files = await this.wrapped.fetchFileList();
|
||||
// console.log(files);
|
||||
return Promise.all(
|
||||
files.filter((fileMeta) =>
|
||||
fileMeta.name !== this.encryptedKeyFileName &&
|
||||
fileMeta.name !== saltFile
|
||||
)
|
||||
.map(async (fileMeta) => {
|
||||
return {
|
||||
...fileMeta,
|
||||
name: await this.decryptPath(this.masterKey!, fileMeta.name),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getFileMeta(name: string): Promise<FileMeta> {
|
||||
if (this.isUnencryptedPath(name)) {
|
||||
return this.wrapped.getFileMeta(name);
|
||||
}
|
||||
const fileMeta = await this.wrapped.getFileMeta(
|
||||
await this.encryptPath(this.masterKey!, name),
|
||||
);
|
||||
return {
|
||||
...fileMeta,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
async readFile(name: string): Promise<{ data: Uint8Array; meta: FileMeta }> {
|
||||
if (this.isUnencryptedPath(name)) {
|
||||
return this.wrapped.readFile(name);
|
||||
}
|
||||
const { data, meta } = await this.wrapped.readFile(
|
||||
await this.encryptPath(this.masterKey!, name),
|
||||
);
|
||||
return {
|
||||
data: await this.decryptAES(this.masterKey!, data),
|
||||
meta: {
|
||||
...meta,
|
||||
name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async writeFile(
|
||||
name: string,
|
||||
data: Uint8Array,
|
||||
selfUpdate?: boolean | undefined,
|
||||
meta?: FileMeta | undefined,
|
||||
): Promise<FileMeta> {
|
||||
if (this.isUnencryptedPath(name)) {
|
||||
return this.wrapped.writeFile(name, data, selfUpdate, meta);
|
||||
}
|
||||
const newMeta = await this.wrapped.writeFile(
|
||||
await this.encryptPath(this.masterKey!, name),
|
||||
await this.encryptAES(this.masterKey!, data),
|
||||
selfUpdate,
|
||||
meta,
|
||||
);
|
||||
return {
|
||||
...newMeta,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteFile(name: string): Promise<void> {
|
||||
if (this.isUnencryptedPath(name)) {
|
||||
return this.wrapped.deleteFile(name);
|
||||
}
|
||||
return this.wrapped.deleteFile(
|
||||
await this.encryptPath(this.masterKey!, name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function removePadding(str: string, paddingChar: string): string {
|
||||
// let startIndex = 0;
|
||||
// while (startIndex < str.length && str[startIndex] === paddingChar) {
|
||||
// startIndex++;
|
||||
// }
|
||||
// return str.substring(startIndex);
|
||||
let endIndex = str.length - 1;
|
||||
while (endIndex >= 0 && str[endIndex] === paddingChar) {
|
||||
endIndex--;
|
||||
}
|
||||
return str.substring(0, endIndex + 1);
|
||||
}
|
||||
|
||||
function base32Encode(data: Uint8Array): string {
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
let result = "";
|
||||
let bits = 0;
|
||||
let value = 0;
|
||||
for (const byte of data) {
|
||||
value = (value << 8) | byte;
|
||||
bits += 8;
|
||||
while (bits >= 5) {
|
||||
result += alphabet[(value >>> (bits - 5)) & 31];
|
||||
bits -= 5;
|
||||
}
|
||||
}
|
||||
if (bits > 0) {
|
||||
result += alphabet[(value << (5 - bits)) & 31];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function base32Decode(data: string): Uint8Array {
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
const result = new Uint8Array(Math.floor(data.length * 5 / 8));
|
||||
let bits = 0;
|
||||
let value = 0;
|
||||
let index = 0;
|
||||
for (const char of data) {
|
||||
value = (value << 5) | alphabet.indexOf(char);
|
||||
bits += 5;
|
||||
if (bits >= 8) {
|
||||
result[index++] = (value >>> (bits - 8)) & 255;
|
||||
bits -= 8;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function appendBuffer(buffer1: Uint8Array, buffer2: Uint8Array): Uint8Array {
|
||||
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
|
||||
tmp.set(new Uint8Array(buffer1), 0);
|
||||
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
|
||||
return tmp;
|
||||
}
|
||||
@@ -35,8 +35,8 @@ export async function testSpacePrimitives(spacePrimitives: SpacePrimitives) {
|
||||
buf.set([1, 2, 3, 4, 5]);
|
||||
// Write binary file
|
||||
await spacePrimitives.writeFile("test.bin", buf);
|
||||
const fMeta = await spacePrimitives.getFileMeta("test.bin");
|
||||
assertEquals(fMeta.size, 1024 * 1024);
|
||||
const fileData = await spacePrimitives.readFile("test.bin");
|
||||
assertEquals(fileData.data.length, 1024 * 1024);
|
||||
assertEquals((await spacePrimitives.fetchFileList()).length, 2);
|
||||
// console.log(spacePrimitives);
|
||||
|
||||
|
||||
+10
-2
@@ -1,6 +1,7 @@
|
||||
import { SETTINGS_TEMPLATE } from "./settings_template.ts";
|
||||
import { YAML } from "./deps.ts";
|
||||
import { SpacePrimitives } from "./spaces/space_primitives.ts";
|
||||
import { expandPropertyNames } from "$sb/lib/json.ts";
|
||||
|
||||
export function safeRun(fn: () => Promise<void>) {
|
||||
fn().catch((e) => {
|
||||
@@ -43,6 +44,7 @@ export async function ensureSettingsAndIndex(
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (e.message === "Not found") {
|
||||
console.log("No settings found, creating default settings");
|
||||
await space.writeFile(
|
||||
"SETTINGS.md",
|
||||
new TextEncoder().encode(SETTINGS_TEMPLATE),
|
||||
@@ -56,7 +58,11 @@ export async function ensureSettingsAndIndex(
|
||||
// Ok, then let's also check the index page
|
||||
try {
|
||||
await space.getFileMeta("index.md");
|
||||
} catch {
|
||||
} catch (e: any) {
|
||||
console.log(
|
||||
"No index page found, creating default index page",
|
||||
e.message,
|
||||
);
|
||||
await space.writeFile(
|
||||
"index.md",
|
||||
new TextEncoder().encode(
|
||||
@@ -71,5 +77,7 @@ page: "[[!silverbullet.md/Getting Started]]"
|
||||
}
|
||||
}
|
||||
|
||||
return parseYamlSettings(settingsText);
|
||||
const settings = parseYamlSettings(settingsText);
|
||||
expandPropertyNames(settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user