├── .gitignore ├── README.md ├── deno.json ├── deps.ts ├── helpers ├── pick_replica.ts ├── pick_share.ts └── replica_archive.ts └── scripts ├── add_server.ts ├── add_share.ts ├── archive_share.ts ├── current_author.ts ├── forget_author.ts ├── list_authors.ts ├── list_paths.ts ├── list_servers.ts ├── list_shares.ts ├── new_author.ts ├── new_share.ts ├── remove_server.ts ├── remove_share.ts ├── save_attachment.ts ├── set_author.ts ├── share_info.ts ├── show_share_secret.ts ├── sync_all.ts ├── sync_archive.ts ├── sync_dir.ts ├── sync_with_server.ts └── write_replica.ts /.gitignore: -------------------------------------------------------------------------------- 1 | test_stuff 2 | share_data/* 3 | .DS_Store 4 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Earthstar Scripts 2 | 3 | A collection of [Earthstar](https://earthstar-project.org) scripts for reading, 4 | writing, syncing, and archiving share data. 5 | 6 | It's like a CLI, but better: you can read, modify, and extend the available 7 | scripts so they're just the way you want them. 8 | 9 | All these scripts pull their settings from the same source, so you can reuse a 10 | keypair and all your favourite shares and servers between them. 11 | 12 | All share replica data is persisted to the filesystem in `share_data`. 13 | 14 | ## Set up 15 | 16 | 1. Clone this repository 17 | 2. Install the Deno runtime 18 | ([Instructions](https://deno.land/manual@v1.29.1/getting_started/installation)) 19 | 3. Test it all works with `deno run scripts/new_author.ts suzy` 20 | 21 | ## Available scripts 22 | 23 | ### `add_server.ts` 24 | 25 | Add the URL of a server to the shared settings so that it can be synced with. 26 | 27 | ### `add_share.ts` 28 | 29 | Adds an existing share to the shared settings so that other scripts can use it. 30 | The first argument is the URL to be saved. 31 | 32 | ### `archive_share.ts` 33 | 34 | Archives a share replica's data to a zip file. You can then put that archive on 35 | a USB key and give it to a friend for syncing, or back it up. 36 | 37 | ### `current_author.ts` 38 | 39 | Display the current author keypair saved to shared settings. 40 | 41 | ### `forget_author.ts` 42 | 43 | Forget the current author keypair saved to shared settings. 44 | 45 | ### `list_authors.ts` 46 | 47 | Lists all authors from a share replica. 48 | 49 | ### `list_paths.ts` 50 | 51 | Lists all document paths from a share replica. 52 | 53 | ### `list_servers.ts` 54 | 55 | List all servers saved to shared settings. 56 | 57 | ### `list_shares.ts` 58 | 59 | List all shares saved to shared settings. 60 | 61 | ### `new_author.ts` 62 | 63 | Generates a new author keypair from a shortname and adds it to shared settings 64 | for other scripts to use. The sole argument of this script is the four character 65 | shortname used in the new keypair. 66 | 67 | ### `new_share.ts` 68 | 69 | Generates a new share keypair from a name and adds it to shared settings for 70 | other scripts to use. 71 | 72 | ### `remove_server.ts` 73 | 74 | Removes a server from the list of servers known by `SharedSettings`. 75 | 76 | ### `remove_share.ts` 77 | 78 | Removes a share from the list of known shares. 79 | 80 | ### `save_attachment.ts` 81 | 82 | Write an attachment to your filesystem. 83 | 84 | ### `set_author.ts` 85 | 86 | Take an existing author keypair and save it to the shared settings for other 87 | scripts to use. 88 | 89 | ### `share_info.ts` 90 | 91 | Prints some info about a share replica. 92 | 93 | ### `show_share_secret.ts` 94 | 95 | Displays the secret of a chosen share. 96 | 97 | ### `sync_archive.ts` 98 | 99 | Takes a zipped share archive, and syncs it with our own data for that share. 100 | Optionally updates the zipped archive. Useful for when you get that USB key from 101 | your friend. 102 | 103 | ### `sync_dir.ts` 104 | 105 | Sync the contents of a filesystem directory with a share replica. 106 | 107 | ### `sync_all.ts` 108 | 109 | Sync all known shares with all known servers (only shares held in common by both 110 | sides will be synced). 111 | 112 | ### `sync_with_server.ts` 113 | 114 | Sync one share replica with one Earthstar server. 115 | 116 | ### `write_replica` 117 | 118 | Write some data to a share replica with free-form text or a file on the 119 | filesystem. 120 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false 3 | } 4 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export * as Earthstar from "https://deno.land/x/earthstar@v10.0.1/mod.ts"; 2 | export { 3 | Input, 4 | Select, 5 | } from "https://deno.land/x/cliffy@v0.25.5/prompt/mod.ts"; 6 | -------------------------------------------------------------------------------- /helpers/pick_replica.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar } from "../deps.ts"; 2 | import { pickShare } from "./pick_share.ts"; 3 | 4 | export async function pickReplica() { 5 | const shareKeypair = await pickShare(); 6 | 7 | return new Earthstar.Replica({ 8 | driver: new Earthstar.ReplicaDriverFs( 9 | shareKeypair.address, 10 | `./share_data/${shareKeypair.address}/`, 11 | ), 12 | shareSecret: shareKeypair.secret, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /helpers/pick_share.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Select } from "../deps.ts"; 2 | 3 | export async function pickShare(): Promise<{ 4 | address: string; 5 | secret: string | undefined; 6 | }> { 7 | const settings = new Earthstar.SharedSettings(); 8 | 9 | if (settings.shares.length === 0) { 10 | throw "No known shares."; 11 | } 12 | 13 | const share = await Select.prompt({ 14 | message: "Pick a share", 15 | options: settings.shares, 16 | }); 17 | 18 | return { address: share, secret: settings.shareSecrets[share] }; 19 | } 20 | -------------------------------------------------------------------------------- /helpers/replica_archive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | dirname, 3 | join, 4 | relative, 5 | } from "https://deno.land/std@0.158.0/path/mod.ts"; 6 | import { Earthstar } from "../deps.ts"; 7 | import { Tar, Untar } from "https://deno.land/std@0.158.0/archive/tar.ts"; 8 | import { 9 | ensureDir, 10 | ensureFile, 11 | walk, 12 | } from "https://deno.land/std@0.158.0/fs/mod.ts"; 13 | import { copy } from "https://deno.land/std@0.158.0/streams/conversion.ts"; 14 | import {} from "https://deno.land/std@0.158.0/path/mod.ts"; 15 | 16 | export async function replicaFromArchive( 17 | { shareAddress, archivePath, fsDriverPath, shareSecret }: { 18 | shareAddress: string; 19 | archivePath: string; 20 | fsDriverPath: string; 21 | shareSecret?: string; 22 | }, 23 | ): Promise { 24 | const dir = dirname(archivePath); 25 | await Deno.lstat(dir); 26 | 27 | // If there is already an archive at the path, put it in the temporary directory. 28 | try { 29 | const reader = await Deno.open(archivePath, { read: true }); 30 | const untar = new Untar(reader); 31 | 32 | for await (const entry of untar) { 33 | if (entry.type === "directory") { 34 | await ensureDir(join(fsDriverPath, entry.fileName)); 35 | continue; 36 | } 37 | 38 | await ensureFile(join(fsDriverPath, entry.fileName)); 39 | const file = await Deno.open(join(fsDriverPath, entry.fileName), { 40 | write: true, 41 | }); 42 | // is a reader. 43 | await copy(entry, file); 44 | } 45 | } catch { 46 | // There's no pre-existing archive, that's fine too. 47 | await ensureDir(join(fsDriverPath, "attachments", "staging")); 48 | await ensureDir(join(fsDriverPath, "attachments", "staging", "es.5")); 49 | await ensureDir(join(fsDriverPath, "attachments", "es.5")); 50 | } 51 | 52 | const replica = new Earthstar.Replica({ 53 | driver: new Earthstar.ReplicaDriverFs(shareAddress, fsDriverPath), 54 | shareSecret, 55 | }); 56 | 57 | return replica; 58 | } 59 | 60 | export async function replicaToArchive( 61 | fsDriverPath: string, 62 | archivePath: string, 63 | ) { 64 | const tar = new Tar(); 65 | 66 | for await (const entry of walk(fsDriverPath)) { 67 | // Or specifying a filePath. 68 | if (entry.isFile) { 69 | const relativePath = relative(fsDriverPath, entry.path); 70 | 71 | await tar.append(relativePath, { 72 | filePath: entry.path, 73 | }); 74 | } 75 | } 76 | 77 | const writer = await Deno.open(archivePath, { write: true, create: true }); 78 | await copy(tar.getReader(), writer); 79 | writer.close(); 80 | } 81 | -------------------------------------------------------------------------------- /scripts/add_server.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar } from "../deps.ts"; 2 | 3 | const serverUrl = Deno.args[0]; 4 | 5 | const settings = new Earthstar.SharedSettings(); 6 | 7 | const result = settings.addServer(serverUrl); 8 | 9 | if (Earthstar.isErr(result)) { 10 | console.error(result.message); 11 | Deno.exit(1); 12 | } 13 | 14 | console.log(`Added ${serverUrl} to known servers.`); 15 | 16 | Deno.exit(0); 17 | -------------------------------------------------------------------------------- /scripts/add_share.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Input } from "../deps.ts"; 2 | 3 | const address = await Input.prompt({ 4 | message: "What is the share's address?", 5 | }); 6 | 7 | const secret = await Input.prompt({ 8 | message: "What is the share's secret? (optional)", 9 | }); 10 | 11 | const settings = new Earthstar.SharedSettings(); 12 | 13 | const addAddressRes = settings.addShare(address); 14 | 15 | if (Earthstar.isErr(addAddressRes)) { 16 | console.error(addAddressRes); 17 | Deno.exit(1); 18 | } 19 | 20 | if (secret.length > 0) { 21 | const addSecretRes = await settings.addSecret(address, secret); 22 | 23 | if (Earthstar.isErr(addSecretRes)) { 24 | console.error(addSecretRes); 25 | Deno.exit(1); 26 | } 27 | } 28 | 29 | console.log(`${address} added to known shares.`); 30 | 31 | Deno.exit(0); 32 | -------------------------------------------------------------------------------- /scripts/archive_share.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Input } from "../deps.ts"; 2 | import { pickShare } from "../helpers/pick_share.ts"; 3 | import { ZipWriter } from "https://deno.land/x/zipjs@v2.6.60/index.js"; 4 | import { walk } from "https://deno.land/std@0.167.0/fs/mod.ts"; 5 | import { join } from "https://deno.land/std@0.167.0/path/mod.ts"; 6 | 7 | const share = await pickShare(); 8 | 9 | const parsed = Earthstar.parseShareAddress( 10 | share.address, 11 | ) as Earthstar.ParsedAddress; 12 | 13 | const outputPath = await Input.prompt({ 14 | message: "Where would you like to save the share archive to?", 15 | 16 | default: `./${parsed.name}.zip`, 17 | }); 18 | 19 | const zipFile = await Deno.open(outputPath, { 20 | create: true, 21 | write: true, 22 | truncate: true, 23 | }); 24 | 25 | const zipWriter = new ZipWriter(zipFile.writable); 26 | 27 | for await (const entry of walk(`./share_data/${share.address}`)) { 28 | const [, ...rest] = entry.path.split("/"); 29 | 30 | const newPath = join(...rest); 31 | 32 | if (entry.isDirectory) { 33 | await zipWriter.add(newPath, undefined, { directory: true }); 34 | 35 | continue; 36 | } 37 | 38 | const file = await Deno.open(entry.path, { read: true }); 39 | 40 | await zipWriter.add(newPath, file.readable); 41 | } 42 | 43 | await zipWriter.close(); 44 | 45 | console.log(`Archived share to ${outputPath}`); 46 | -------------------------------------------------------------------------------- /scripts/current_author.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar } from "../deps.ts"; 2 | 3 | const settings = new Earthstar.SharedSettings(); 4 | 5 | if (!settings.author) { 6 | console.log("No author keypair currently saved to settings."); 7 | Deno.exit(0); 8 | } 9 | 10 | console.group("Current author keypair:"); 11 | console.log(settings.author.address); 12 | 13 | Deno.exit(0); 14 | -------------------------------------------------------------------------------- /scripts/forget_author.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar } from "../deps.ts"; 2 | 3 | const settings = new Earthstar.SharedSettings(); 4 | 5 | if (!settings.author) { 6 | console.log("No author keypair currently saved to settings."); 7 | Deno.exit(0); 8 | } 9 | 10 | console.group("Current author keypair:"); 11 | console.log(settings.author.address); 12 | 13 | const isSure = confirm("Are you sure you want to forget this keypair?"); 14 | 15 | if (isSure) { 16 | settings.author = null; 17 | 18 | console.log("✅ Forgot author keypair"); 19 | } 20 | 21 | Deno.exit(0); 22 | -------------------------------------------------------------------------------- /scripts/list_authors.ts: -------------------------------------------------------------------------------- 1 | import { pickReplica } from "../helpers/pick_replica.ts"; 2 | 3 | const replica = await pickReplica(); 4 | 5 | const allAuthors = await replica.queryAuthors(); 6 | 7 | for (const path of allAuthors) { 8 | console.log(path); 9 | } 10 | 11 | Deno.exit(0); 12 | -------------------------------------------------------------------------------- /scripts/list_paths.ts: -------------------------------------------------------------------------------- 1 | import { pickReplica } from "../helpers/pick_replica.ts"; 2 | 3 | const replica = await pickReplica(); 4 | 5 | const allPaths = await replica.queryPaths(); 6 | 7 | for (const path of allPaths) { 8 | console.log(path); 9 | } 10 | 11 | Deno.exit(0); 12 | -------------------------------------------------------------------------------- /scripts/list_servers.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar } from "../deps.ts"; 2 | 3 | const settings = new Earthstar.SharedSettings(); 4 | 5 | if (settings.servers.length === 0) { 6 | console.log("No servers saved to shared settings."); 7 | } 8 | 9 | for (const share of settings.servers) { 10 | console.log(share); 11 | } 12 | -------------------------------------------------------------------------------- /scripts/list_shares.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar } from "../deps.ts"; 2 | 3 | const settings = new Earthstar.SharedSettings(); 4 | 5 | if (settings.shares.length === 0) { 6 | console.log("No shares saved to shared settings."); 7 | } 8 | 9 | for (const share of settings.shares) { 10 | console.log(share); 11 | } 12 | -------------------------------------------------------------------------------- /scripts/new_author.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar } from "../deps.ts"; 2 | 3 | const name = Deno.args[0]; 4 | 5 | if (!name) { 6 | console.error( 7 | "You need to provide a 4 character short name for the new keypair.", 8 | ); 9 | Deno.exit(1); 10 | } 11 | 12 | const result = await Earthstar.Crypto.generateAuthorKeypair(name); 13 | 14 | if (Earthstar.isErr(result)) { 15 | console.error(result.message); 16 | Deno.exit(1); 17 | } 18 | 19 | const settings = new Earthstar.SharedSettings(); 20 | 21 | if (settings.author) { 22 | const doesWantToReplace = confirm( 23 | `Settings already has ${settings.author.address} stored. Are you sure you want to replace it?`, 24 | ); 25 | 26 | if (!doesWantToReplace) { 27 | console.log("Aborting..."); 28 | Deno.exit(0); 29 | } 30 | } 31 | 32 | settings.author = result; 33 | 34 | console.group(`New keypair saved to settings.`); 35 | console.log("Author address:", result.address); 36 | console.log("Author secret:", result.secret); 37 | console.groupEnd(); 38 | console.log("Save these somewhere safe."); 39 | 40 | Deno.exit(0); 41 | -------------------------------------------------------------------------------- /scripts/new_share.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar } from "../deps.ts"; 2 | 3 | const name = Deno.args[0]; 4 | 5 | if (!name) { 6 | console.error("You need to provide a name for your new share."); 7 | Deno.exit(1); 8 | } 9 | 10 | const result = await Earthstar.Crypto.generateShareKeypair(name); 11 | 12 | if (Earthstar.isErr(result)) { 13 | console.error(result.message); 14 | Deno.exit(1); 15 | } 16 | 17 | const settings = new Earthstar.SharedSettings(); 18 | 19 | settings.addShare(result.shareAddress); 20 | await settings.addSecret(result.shareAddress, result.secret); 21 | 22 | console.group("Added the new share to your settings."); 23 | console.log("Share address:", result.shareAddress); 24 | console.log("Share secret:", result.secret); 25 | console.groupEnd(); 26 | console.log("Save these somewhere safe."); 27 | console.log("Share the address with people you want to give read access."); 28 | console.log("Share the secret with people you want to give write access to."); 29 | 30 | Deno.exit(0); 31 | -------------------------------------------------------------------------------- /scripts/remove_server.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Select } from "../deps.ts"; 2 | 3 | const settings = new Earthstar.SharedSettings(); 4 | 5 | if (settings.servers.length === 0) { 6 | console.log("There are no known servers to remove!"); 7 | Deno.exit(0); 8 | } 9 | 10 | const choice = await Select.prompt({ 11 | message: "Which server would you like to be forgotten?", 12 | options: settings.servers, 13 | }); 14 | 15 | settings.removeServer(choice); 16 | 17 | console.log("✅ Removed"); 18 | 19 | Deno.exit(0); 20 | -------------------------------------------------------------------------------- /scripts/remove_share.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Select } from "../deps.ts"; 2 | 3 | const settings = new Earthstar.SharedSettings(); 4 | 5 | if (settings.shares.length === 0) { 6 | console.log("There are no known shares to remove!"); 7 | Deno.exit(0); 8 | } 9 | 10 | const choice = await Select.prompt({ 11 | message: "Which server would you like to be forgotten?", 12 | options: settings.shares, 13 | }); 14 | 15 | const isSure = confirm( 16 | `Are you sure you want to forget ${choice} and all its data?`, 17 | ); 18 | 19 | if (!isSure) { 20 | Deno.exit(0); 21 | } 22 | 23 | settings.removeShare(choice); 24 | 25 | const replica = new Earthstar.Replica({ 26 | driver: new Earthstar.ReplicaDriverFs( 27 | choice, 28 | `./share_data/${choice}/`, 29 | ), 30 | }); 31 | 32 | // Delete all the data. 33 | await replica.close(true); 34 | 35 | console.log("✅ Removed"); 36 | 37 | Deno.exit(0); 38 | -------------------------------------------------------------------------------- /scripts/save_attachment.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Input } from "../deps.ts"; 2 | import { pickReplica } from "../helpers/pick_replica.ts"; 3 | 4 | try { 5 | const replica = await pickReplica(); 6 | 7 | const docPath = await Input.prompt({ 8 | message: "Choose a path", 9 | suggestions: await replica.queryPaths({}), 10 | }); 11 | 12 | const doc = await replica.getLatestDocAtPath(docPath); 13 | 14 | if (!doc) { 15 | console.log(`No document with the path ${docPath} found.`); 16 | Deno.exit(1); 17 | } 18 | 19 | const attachment = await replica.getAttachment(doc); 20 | 21 | if (!attachment) { 22 | console.log(`We don't have the attachment for the document at ${docPath}`); 23 | Deno.exit(1); 24 | } 25 | 26 | if (Earthstar.isErr(attachment)) { 27 | console.log(`The document at ${docPath} can't have an attachment.`); 28 | Deno.exit(1); 29 | } 30 | 31 | const outputPath = await Input.prompt({ 32 | message: `Choose a path to save the attachment for ${docPath} to`, 33 | }); 34 | 35 | try { 36 | await Deno.lstat(outputPath); 37 | 38 | console.log(`There is already a file at ${outputPath}`); 39 | Deno.exit(1); 40 | } catch { 41 | // No file present, it's fine to write there. 42 | } 43 | 44 | const stream = await attachment.stream(); 45 | 46 | const file = await Deno.open(outputPath, { create: true, write: true }); 47 | 48 | await stream.pipeTo(file.writable); 49 | 50 | console.log(`Contents of ${docPath} written to ${outputPath}`); 51 | 52 | Deno.exit(0); 53 | } catch (err) { 54 | console.error(err); 55 | Deno.exit(1); 56 | } 57 | -------------------------------------------------------------------------------- /scripts/set_author.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Input } from "../deps.ts"; 2 | 3 | const address = await Input.prompt({ 4 | message: "What is the existing keypair's address?", 5 | }); 6 | 7 | const secret = await Input.prompt({ 8 | message: "What is the existing keypair's secret?", 9 | }); 10 | 11 | const settings = new Earthstar.SharedSettings(); 12 | 13 | if (settings.author) { 14 | const doesWantToReplace = confirm( 15 | `Settings already has ${settings.author.address} stored. Are you sure you want to replace it?`, 16 | ); 17 | 18 | if (!doesWantToReplace) { 19 | console.log("Aborting..."); 20 | Deno.exit(0); 21 | } 22 | } 23 | 24 | settings.author = { address, secret }; 25 | 26 | console.log(`Existing keypair saved to settings.`); 27 | 28 | Deno.exit(0); 29 | -------------------------------------------------------------------------------- /scripts/share_info.ts: -------------------------------------------------------------------------------- 1 | import { pickReplica } from "../helpers/pick_replica.ts"; 2 | 3 | try { 4 | const replica = await pickReplica(); 5 | 6 | const allDocs = await replica.getAllDocs(); 7 | const allAuthors = await replica.queryAuthors(); 8 | 9 | let latestTimestamp = 0; 10 | 11 | for (const doc of allDocs) { 12 | if (doc.timestamp > latestTimestamp) { 13 | latestTimestamp = doc.timestamp; 14 | } 15 | } 16 | 17 | console.group(replica.share); 18 | console.log("Number of docs:", allDocs.length); 19 | console.log("Number of authors:", allAuthors.length); 20 | console.log( 21 | "Last updated:", 22 | new Date(latestTimestamp / 1000).toLocaleString(), 23 | ); 24 | 25 | console.groupEnd(); 26 | 27 | Deno.exit(0); 28 | } catch (err) { 29 | console.log(err); 30 | Deno.exit(1); 31 | } 32 | -------------------------------------------------------------------------------- /scripts/show_share_secret.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Select } from "../deps.ts"; 2 | 3 | const settings = new Earthstar.SharedSettings(); 4 | 5 | if (settings.shares.length === 0) { 6 | console.log("There are no known shares to show secrets for!"); 7 | Deno.exit(0); 8 | } 9 | 10 | const choice = await Select.prompt({ 11 | message: "Which server would you like to show the secret for?", 12 | options: settings.shares, 13 | }); 14 | 15 | const secret = settings.shareSecrets[choice]; 16 | 17 | if (!secret) { 18 | console.log("We don't know the secret for that share..."); 19 | Deno.exit(1); 20 | } 21 | 22 | console.log(secret); 23 | Deno.exit(0); 24 | -------------------------------------------------------------------------------- /scripts/sync_all.ts: -------------------------------------------------------------------------------- 1 | // Sync all known shares with all known servers. 2 | 3 | import { Earthstar } from "../deps.ts"; 4 | 5 | const settings = new Earthstar.SharedSettings(); 6 | 7 | const peer = new Earthstar.Peer(); 8 | 9 | for (const share of settings.shares) { 10 | const replica = new Earthstar.Replica({ 11 | driver: new Earthstar.ReplicaDriverFs( 12 | share, 13 | `./share_data/${share}/`, 14 | ), 15 | }); 16 | 17 | peer.addReplica(replica); 18 | } 19 | 20 | const syncOps = settings.servers.map((serverUrl) => { 21 | const syncer = peer.sync(serverUrl); 22 | 23 | syncer.isDone().then(() => { 24 | console.log(`✅ Synced with ${serverUrl}`); 25 | }).catch((err: Earthstar.EarthstarError) => { 26 | console.group(`❌ Sync with ${serverUrl} failed`); 27 | console.log(err); 28 | console.groupEnd(); 29 | }); 30 | 31 | return syncer.isDone(); 32 | }); 33 | 34 | await Promise.allSettled(syncOps); 35 | 36 | Deno.exit(0); 37 | -------------------------------------------------------------------------------- /scripts/sync_archive.ts: -------------------------------------------------------------------------------- 1 | import { ensureDir } from "https://deno.land/std@0.167.0/fs/mod.ts"; 2 | import { walk } from "https://deno.land/std@0.167.0/fs/walk.ts"; 3 | import { join } from "https://deno.land/std@0.167.0/path/mod.ts"; 4 | import { 5 | ZipReader, 6 | ZipWriter, 7 | } from "https://deno.land/x/zipjs@v2.6.60/index.js"; 8 | import { Earthstar } from "../deps.ts"; 9 | 10 | const archivePath = Deno.args[0]; 11 | 12 | // Unzip 13 | const zipFile = await Deno.open(archivePath, { read: true }); 14 | 15 | const zipReader = new ZipReader(zipFile.readable); 16 | 17 | const tempDir = await Deno.makeTempDir(); 18 | 19 | let shareName; 20 | 21 | for await (const entry of zipReader.getEntriesGenerator()) { 22 | const pathToWriteTo = join(tempDir, entry.filename); 23 | 24 | if (!shareName) { 25 | shareName = entry.filename.split("/")[0]; 26 | } 27 | 28 | if (entry.directory) { 29 | await ensureDir(pathToWriteTo); 30 | continue; 31 | } 32 | 33 | const file = await Deno.open(pathToWriteTo, { create: true, write: true }); 34 | 35 | await entry.getData(file.writable); 36 | } 37 | 38 | await zipReader.close(); 39 | 40 | console.log("derived name", shareName); 41 | 42 | // Derive share name from unzipped contents. 43 | 44 | // Create a new replica with unzipped contents 45 | const zipReplica = new Earthstar.Replica({ 46 | driver: new Earthstar.ReplicaDriverFs( 47 | shareName as string, 48 | join(tempDir, shareName as string), 49 | ), 50 | }); 51 | 52 | // Sync it with new replica with data in share_data 53 | const ourReplica = new Earthstar.Replica({ 54 | driver: new Earthstar.ReplicaDriverFs( 55 | shareName as string, 56 | `./share_data/${shareName}`, 57 | ), 58 | }); 59 | 60 | const peerA = new Earthstar.Peer(); 61 | peerA.addReplica(zipReplica); 62 | 63 | const peerB = new Earthstar.Peer(); 64 | peerB.addReplica(ourReplica); 65 | 66 | const syncer = peerA.sync(peerB); 67 | 68 | console.log("Syncing..."); 69 | await syncer.isDone(); 70 | console.log("Done."); 71 | 72 | const shouldModifyZip = confirm( 73 | `Overwrite ${archivePath} with freshly synced archive?`, 74 | ); 75 | 76 | if (shouldModifyZip) { 77 | const zipFile2 = await Deno.open(archivePath, { 78 | write: true, 79 | truncate: true, 80 | }); 81 | 82 | const zipWriter = new ZipWriter(zipFile2.writable); 83 | 84 | for await (const entry of walk(join(tempDir, shareName as string))) { 85 | const pathBits = entry.path.split("/"); 86 | 87 | const indexOfShare = pathBits.indexOf(shareName as string); 88 | const newPathBits = pathBits.slice(indexOfShare); 89 | const newPath = join(...newPathBits); 90 | 91 | if (entry.isDirectory) { 92 | await zipWriter.add(newPath, undefined, { directory: true }); 93 | continue; 94 | } 95 | 96 | const file = await Deno.open(entry.path, { read: true }); 97 | 98 | await zipWriter.add(newPath, file.readable); 99 | } 100 | 101 | await zipWriter.close(); 102 | 103 | console.log(`Updated ${archivePath}`); 104 | } 105 | 106 | // Clean up zip replica 107 | await zipReplica.close(true); 108 | await ourReplica.close(false); 109 | await Deno.remove(tempDir, { 110 | recursive: true, 111 | }); 112 | -------------------------------------------------------------------------------- /scripts/sync_dir.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Input } from "../deps.ts"; 2 | import { pickReplica } from "../helpers/pick_replica.ts"; 3 | 4 | const settings = new Earthstar.SharedSettings(); 5 | 6 | if (!settings.author) { 7 | console.log("Can't use this script without an author keypair in settings."); 8 | console.log("Either add an existing keypair or generate a new one."); 9 | Deno.exit(1); 10 | } 11 | 12 | const replica = await pickReplica(); 13 | 14 | const dirPath = await Input.prompt({ 15 | message: `Where is the directory you'd like to sync with this share?`, 16 | }); 17 | 18 | await Earthstar.syncReplicaAndFsDir({ 19 | replica, 20 | dirPath, 21 | keypair: settings.author, 22 | allowDirtyDirWithoutManifest: true, 23 | overwriteFilesAtOwnedPaths: true, 24 | }); 25 | 26 | await replica.close(false); 27 | 28 | console.log(`Synced ${replica.share} with ${dirPath}`); 29 | -------------------------------------------------------------------------------- /scripts/sync_with_server.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Input } from "../deps.ts"; 2 | import { pickReplica } from "../helpers/pick_replica.ts"; 3 | 4 | // This script syncs a share with a remote replica server, and persists the results to disk. 5 | // If a path to an existing archive is given, it will use that. 6 | 7 | const replica = await pickReplica(); 8 | 9 | // Put it in a peer for syncing 10 | const peer = new Earthstar.Peer(); 11 | 12 | peer.addReplica(replica); 13 | 14 | console.log("Syncing..."); 15 | 16 | const settings = new Earthstar.SharedSettings(); 17 | 18 | const url = await Input.prompt({ 19 | message: "What is the URL of the server you'd you like to sync with?", 20 | suggestions: settings.servers, 21 | }); 22 | 23 | try { 24 | new URL(url); 25 | } catch { 26 | console.error(`${url} is not a valid URL.`); 27 | Deno.exit(1); 28 | } 29 | 30 | if (!settings.servers.includes(new URL(url).toString())) { 31 | const willSave = confirm(`Save ${url} to favourite servers?`); 32 | 33 | if (willSave) { 34 | settings.addServer(url); 35 | } 36 | } 37 | 38 | console.log("Syncing..."); 39 | 40 | // Start syncing and wait until finished. 41 | const syncer = peer.sync(url); 42 | 43 | syncer.onStatusChange((newStatus) => { 44 | let allRequestedDocs = 0; 45 | let allReceivedDocs = 0; 46 | let allSentDocs = 0; 47 | let transfersInProgress = 0; 48 | 49 | for (const share in newStatus) { 50 | const shareStatus = newStatus[share]; 51 | 52 | allRequestedDocs += shareStatus.docs.requestedCount; 53 | allReceivedDocs += shareStatus.docs.receivedCount; 54 | allSentDocs += shareStatus.docs.sentCount; 55 | 56 | const transfersWaiting = shareStatus.attachments.filter((transfer) => { 57 | return transfer.status === "ready" || transfer.status === "in_progress"; 58 | }); 59 | 60 | transfersInProgress += transfersWaiting.length; 61 | } 62 | 63 | console.log( 64 | `Syncing ${ 65 | Object.keys(newStatus).length 66 | } shares, got ${allReceivedDocs}/${allRequestedDocs}, sent ${allSentDocs}, ${transfersInProgress} attachment transfers in progress.`, 67 | ); 68 | }); 69 | 70 | await syncer.isDone(); 71 | await replica.close(false); 72 | 73 | console.log("Done!"); 74 | 75 | Deno.exit(0); 76 | -------------------------------------------------------------------------------- /scripts/write_replica.ts: -------------------------------------------------------------------------------- 1 | import { Earthstar, Input, Select } from "../deps.ts"; 2 | import { pickReplica } from "../helpers/pick_replica.ts"; 3 | 4 | const fileExtensionRegex = /^.*\.(\w+)$/; 5 | const endingWithKeypairAddrRegex = /^.*~@\w{4}\.\w{53}$/; 6 | 7 | function pathHasFileExtension(path: string): boolean { 8 | if (path.indexOf(".") === -1) { 9 | return false; 10 | } 11 | 12 | // Check that it's in the right position. 13 | const matches = path.match(fileExtensionRegex); 14 | 15 | if (matches === null) { 16 | return false; 17 | } 18 | 19 | const extension = matches[1]; 20 | 21 | // Is this part of a keypair address? 22 | if (extension.length === 53 && path.match(endingWithKeypairAddrRegex)) { 23 | return false; 24 | } 25 | 26 | return true; 27 | } 28 | 29 | const settings = new Earthstar.SharedSettings(); 30 | 31 | if (!settings.author) { 32 | console.error( 33 | "You can't write data without an author keypair. There isn't one saved in the settings.", 34 | ); 35 | Deno.exit(1); 36 | } 37 | 38 | const replica = await pickReplica(); 39 | 40 | // Check the path. 41 | // If it ends in an extension, pipe in bytes to attachment with auto generated document text (TODO: make this an option) 42 | // If it's not, just read the text and write straight to the doc. 43 | const docPath = await Input.prompt({ 44 | message: "Choose a path", 45 | suggestions: await replica.queryPaths({}), 46 | }); 47 | 48 | const shouldMakeAttachment = pathHasFileExtension(docPath); 49 | 50 | if (shouldMakeAttachment) { 51 | const path = await Input.prompt({ 52 | message: "Choose a filesystem path to read data from", 53 | files: true, 54 | }); 55 | 56 | const file = await Deno.open(path); 57 | 58 | const result = await replica.set(settings.author, { 59 | path: docPath, 60 | text: "This document was generated by my cool script!", 61 | attachment: file.readable, 62 | }); 63 | 64 | if (Earthstar.isErr(result)) { 65 | console.log(result.message); 66 | Deno.exit(1); 67 | } 68 | 69 | await replica.close(false); 70 | 71 | console.log(`Wrote contents of ${path} to ${docPath}`); 72 | Deno.exit(0); 73 | } else { 74 | const text = await Input.prompt({ 75 | message: "Enter document text", 76 | }); 77 | 78 | const result = await replica.set(settings.author, { 79 | path: docPath, 80 | text: text, 81 | }); 82 | 83 | if (Earthstar.isErr(result)) { 84 | console.log(result.message); 85 | Deno.exit(1); 86 | } 87 | 88 | await replica.close(false); 89 | 90 | console.log(`Wrote text to ${docPath}`); 91 | Deno.exit(0); 92 | } 93 | --------------------------------------------------------------------------------