Skip to content

Instantly share code, notes, and snippets.

@pengx17
Last active September 4, 2023 13:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pengx17/6e51885d892b0ce2a29e5f5c0e2d84fe to your computer and use it in GitHub Desktop.
Save pengx17/6e51885d892b0ce2a29e5f5c0e2d84fe to your computer and use it in GitHub Desktop.
Export app.affine.pro and import into AFFiNE client
(async () => {
async function blobToBase64(blob) {
return await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = function() {
resolve(reader.result.split(',')[1]);
}
reader.readAsDataURL(blob);
});
}
async function exportData() {
const Y = currentWorkspace.blockSuiteWorkspace.constructor.Y;
const meta = currentWorkspace.blockSuiteWorkspace.meta;
function getYDocUpdates() {
return Y.encodeStateAsUpdate(currentWorkspace.blockSuiteWorkspace.doc)
}
async function getBlobs() {
const pages = currentWorkspace.blockSuiteWorkspace._pages;
const blobMap = new Map();
let allBlobIds = new Set();
for (let [_, page] of pages) {
for (let [_, block] of page._blockMap) {
if (block.flavour === 'affine:embed' && block.type === 'image') {
const blobId = block.sourceId;
allBlobIds.add(blobId);
}
}
}
if (meta.avatar) {
allBlobIds.add(meta.avatar);
}
allBlobIds = Array.from(allBlobIds);
for (let blobId of allBlobIds) {
console.log('downloading', blobId, `(${allBlobIds.indexOf(blobId) + 1}/${allBlobIds.length})`);
const blob = await currentWorkspace.blockSuiteWorkspace.blobs.get(blobId);
blobMap.set(blobId, blob);
}
console.log('done downloading blobs');
return Array.from(blobMap.entries());
}
const data = {
id: currentWorkspace.id,
name: meta.name,
updates: getYDocUpdates(),
blobs: await getBlobs()
}
return data;
}
const fileSaver = await import('https://esm.sh/file-saver@2.0.5');
const data = await exportData();
console.log(data)
let result = '';
result += `id: ${data.id}\n`;
result += `name: ${data.name}\n`;
result += `updates: ${await blobToBase64(new Blob([data.updates]))}\n`;
for (let [key, blob] of data.blobs) {
result += `blob: ${key}\n`;
result += `blobData: ${await blobToBase64(blob)}\n`;
}
const blob = new Blob([result], { type: 'text/plain;charset=utf-8' });
fileSaver.default.saveAs(blob, `affine-${data.id}-${data.name}-exported.data`);
})()
(async () => {
const idb = await import('https://esm.sh/idb@7.1.1');
const ydocDBId = 'affine-local';
async function openFileChooser() {
return new Promise((resolve) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.onchange = function() {
const file = fileInput.files[0];
// use FileReader to read as text content
const reader = new FileReader();
reader.onload = function() {
resolve(reader.result);
};
reader.readAsText(file);
};
fileInput.click();
});
}
function b64ToUint8(base64) {
// Remove data URL prefix if it exists
const base64WithoutPrefix = base64.split(',').pop();
// Decode the base64 string into a Uint8Array
const binaryString = atob(base64WithoutPrefix);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
async function parseContent(content) {
const lines = content.split('\n');
const data = {
};
for (let line of lines) {
const [key, value] = line.split(': ');
if (key === 'id') {
data.id = value;
} else if (key === 'name') {
data.name = value;
} else if (key === 'updates') {
data.updates = b64ToUint8(value);
} else if (key === 'blob') {
data.blobs = data.blobs || [];
data.blobs.push({
id: value
});
} else if (key === 'blobData') {
data.blobs[data.blobs.length - 1].data = b64ToUint8(value).buffer;
}
}
return data;
}
async function importData({ id, updates, blobs }) {
async function importUpdates() {
const db = await idb.openDB(ydocDBId, 1, {
upgrade(db) {
db.createObjectStore('workspace', { keyPath: 'id' });
}
});
const t = db.transaction('workspace', 'readwrite').objectStore('workspace');
await t.put({
id,
updates: [{
timestamp: Date.now(),
update: new Uint8Array(updates)
}]
});
await t.done;
console.log(`put ydoc done`);
}
async function importBlobs() {
// import blob stores
const db = await idb.openDB(id + '_blob', 1, {
upgrade(db) {
db.createObjectStore('blob');
}
});
const t = db.transaction('blob', 'readwrite').objectStore('blob');
for (let { id, data } of blobs) {
await t.put(new Uint8Array(data), id);
}
await t.done;
console.log(`put blob done ${blobs.length}`);
}
await importUpdates();
await importBlobs();
}
const content = await openFileChooser();
const data = await parseContent(content);
console.log(data)
await importData(data);
const id = data.id;
// put them to local storage
const ls = {
'affine-local-workspace': JSON.parse(localStorage.getItem('affine-local-workspace') || '[]'),
'jotai-workspaces': JSON.parse(localStorage.getItem('jotai-workspaces') || '[]')
};
ls['affine-local-workspace'].push(id);
ls['jotai-workspaces'].push({
id,
flavour: 'local'
});
// write back to local storage
localStorage.setItem('affine-local-workspace', JSON.stringify(ls['affine-local-workspace']));
localStorage.setItem('jotai-workspaces', JSON.stringify(ls['jotai-workspaces']));
localStorage.setItem('last_workspace_id', id);
console.log('imported done for', id, data.name);
console.log('now you can reload the page to see the newly imported workspace')
})();
@pengx17
Copy link
Author

pengx17 commented May 14, 2023

How to use

  1. Run export.js in app.affine.pro, you will export the CURRENT workspace as affine-${data.id}-${data.name}-exported.data. If your workspace has a lot of blobs, it will take a few minutes to complete.
  2. Run import.js in AFFiNE clinet and select affine-${data.id}-${data.name}-exported.data. Now the workspace data will be imported into the client.

NOTE: this script is super experimental and no real tests are involved!!!

20230515-013329.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment