2023-05-14 13:52:47 +02:00
|
|
|
import { Adapters, get, info, setBase } from '@cryptgeon/shared'
|
|
|
|
import inquirer from 'inquirer'
|
|
|
|
import { access, constants, writeFile } from 'node:fs/promises'
|
|
|
|
import { basename, resolve } from 'node:path'
|
2023-05-23 09:39:00 +02:00
|
|
|
import { AES, Hex } from 'occulto'
|
2023-05-14 13:52:47 +02:00
|
|
|
import pretty from 'pretty-bytes'
|
|
|
|
|
2023-05-25 10:16:44 +02:00
|
|
|
export async function download(url: URL, all: boolean, suggestedPassword?: string) {
|
2023-05-14 13:52:47 +02:00
|
|
|
setBase(url.origin)
|
|
|
|
const id = url.pathname.split('/')[2]
|
2024-03-03 01:22:39 +01:00
|
|
|
const preview = await info(id).catch(() => {
|
|
|
|
throw new Error('Note does not exist or is expired')
|
|
|
|
})
|
2023-05-23 09:39:00 +02:00
|
|
|
|
|
|
|
// Password
|
|
|
|
let password: string
|
|
|
|
const derivation = preview?.meta.derivation
|
|
|
|
if (derivation) {
|
2023-05-25 10:16:44 +02:00
|
|
|
if (suggestedPassword) {
|
|
|
|
password = suggestedPassword
|
|
|
|
} else {
|
|
|
|
const response = await inquirer.prompt([
|
|
|
|
{
|
|
|
|
type: 'password',
|
|
|
|
message: 'Note password',
|
|
|
|
name: 'password',
|
|
|
|
},
|
|
|
|
])
|
|
|
|
password = response.password
|
|
|
|
}
|
2023-05-23 09:39:00 +02:00
|
|
|
} else {
|
|
|
|
password = url.hash.slice(1)
|
|
|
|
}
|
2023-05-14 13:52:47 +02:00
|
|
|
|
2023-05-23 09:39:00 +02:00
|
|
|
const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password)
|
|
|
|
const note = await get(id)
|
2023-05-14 13:52:47 +02:00
|
|
|
|
2024-03-03 01:22:39 +01:00
|
|
|
const couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password')
|
2023-05-14 13:52:47 +02:00
|
|
|
switch (note.meta.type) {
|
|
|
|
case 'file':
|
2024-03-03 01:22:39 +01:00
|
|
|
const files = await Adapters.Files.decrypt(note.contents, key).catch(() => {
|
|
|
|
throw couldNotDecrypt
|
|
|
|
})
|
2023-05-14 13:52:47 +02:00
|
|
|
if (!files) {
|
2024-03-03 01:22:39 +01:00
|
|
|
throw new Error('No files found in note')
|
2023-05-14 13:52:47 +02:00
|
|
|
}
|
|
|
|
|
2023-05-25 10:16:44 +02:00
|
|
|
let selected: typeof files
|
|
|
|
if (all) {
|
|
|
|
selected = files
|
|
|
|
} else {
|
|
|
|
const { names } = await inquirer.prompt([
|
|
|
|
{
|
|
|
|
type: 'checkbox',
|
|
|
|
message: 'What files should be saved?',
|
|
|
|
name: 'names',
|
|
|
|
choices: files.map((file) => ({
|
|
|
|
value: file.name,
|
|
|
|
name: `${file.name} - ${file.type} - ${pretty(file.size, { binary: true })}`,
|
|
|
|
checked: true,
|
|
|
|
})),
|
|
|
|
},
|
|
|
|
])
|
|
|
|
selected = files.filter((file) => names.includes(file.name))
|
|
|
|
}
|
2023-05-14 13:52:47 +02:00
|
|
|
|
2024-03-03 01:22:39 +01:00
|
|
|
if (!selected.length) throw new Error('No files selected')
|
2023-05-14 13:52:47 +02:00
|
|
|
await Promise.all(
|
2023-05-25 10:16:44 +02:00
|
|
|
selected.map(async (file) => {
|
2023-05-14 13:52:47 +02:00
|
|
|
let filename = resolve(file.name)
|
|
|
|
try {
|
|
|
|
// If exists -> prepend timestamp to not overwrite the current file
|
|
|
|
await access(filename, constants.R_OK)
|
|
|
|
filename = resolve(`${Date.now()}-${file.name}`)
|
|
|
|
} catch {}
|
|
|
|
await writeFile(filename, file.contents)
|
|
|
|
console.log(`Saved: ${basename(filename)}`)
|
|
|
|
})
|
|
|
|
)
|
2023-05-25 10:16:44 +02:00
|
|
|
|
2023-05-14 13:52:47 +02:00
|
|
|
break
|
|
|
|
case 'text':
|
2024-03-03 01:22:39 +01:00
|
|
|
const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(() => {
|
|
|
|
throw couldNotDecrypt
|
|
|
|
})
|
2023-05-14 13:52:47 +02:00
|
|
|
console.log(plaintext)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|