feat: add desktop repo (#1071)

This commit is contained in:
Mo
2022-06-07 11:52:15 -05:00
committed by GitHub
parent 0bb12db948
commit 0b7ce82aaa
135 changed files with 17821 additions and 180 deletions

View File

@@ -0,0 +1,156 @@
import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
import { AppState } from 'app/application'
import { StoreKeys } from '../Store'
import {
ensureDirectoryExists,
moveDirContents,
openDirectoryPicker,
readJSONFile,
writeFile,
writeJSONFile,
} from '../Utils/FileUtils'
import { FileDownloader } from './FileDownloader'
import { shell } from 'electron'
export const FileBackupsConstantsV1 = {
Version: '1.0.0',
MetadataFileName: 'metadata.sn.json',
BinaryFileName: 'file.encrypted',
}
export class FilesBackupManager implements FileBackupsDevice {
constructor(private appState: AppState) {}
public isFilesBackupsEnabled(): Promise<boolean> {
return Promise.resolve(this.appState.store.get(StoreKeys.FileBackupsEnabled))
}
public async enableFilesBackups(): Promise<void> {
const currentLocation = await this.getFilesBackupsLocation()
if (!currentLocation) {
const result = await this.changeFilesBackupsLocation()
if (!result) {
return
}
}
this.appState.store.set(StoreKeys.FileBackupsEnabled, true)
const mapping = this.getMappingFileFromDisk()
if (!mapping) {
await this.saveFilesBackupsMappingFile(this.defaultMappingFileValue())
}
}
public disableFilesBackups(): Promise<void> {
this.appState.store.set(StoreKeys.FileBackupsEnabled, false)
return Promise.resolve()
}
public async changeFilesBackupsLocation(): Promise<string | undefined> {
const newPath = await openDirectoryPicker()
if (!newPath) {
return undefined
}
const oldPath = await this.getFilesBackupsLocation()
if (oldPath) {
await moveDirContents(oldPath, newPath)
}
this.appState.store.set(StoreKeys.FileBackupsLocation, newPath)
return newPath
}
public getFilesBackupsLocation(): Promise<string> {
return Promise.resolve(this.appState.store.get(StoreKeys.FileBackupsLocation))
}
private getMappingFileLocation(): string {
const base = this.appState.store.get(StoreKeys.FileBackupsLocation)
return `${base}/info.json`
}
private async getMappingFileFromDisk(): Promise<FileBackupsMapping | undefined> {
return readJSONFile<FileBackupsMapping>(this.getMappingFileLocation())
}
private defaultMappingFileValue(): FileBackupsMapping {
return { version: FileBackupsConstantsV1.Version, files: {} }
}
async getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
const data = await this.getMappingFileFromDisk()
if (!data) {
return this.defaultMappingFileValue()
}
return data
}
async openFilesBackupsLocation(): Promise<void> {
const location = await this.getFilesBackupsLocation()
void shell.openPath(location)
}
async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> {
await writeJSONFile(this.getMappingFileLocation(), file)
return 'success'
}
async saveFilesBackupsFile(
uuid: string,
metaFile: string,
downloadRequest: {
chunkSizes: number[]
valetToken: string
url: string
},
): Promise<'success' | 'failed'> {
const backupsDir = await this.getFilesBackupsLocation()
const fileDir = `${backupsDir}/${uuid}`
const metaFilePath = `${fileDir}/${FileBackupsConstantsV1.MetadataFileName}`
const binaryPath = `${fileDir}/${FileBackupsConstantsV1.BinaryFileName}`
await ensureDirectoryExists(fileDir)
await writeFile(metaFilePath, metaFile)
const downloader = new FileDownloader(
downloadRequest.chunkSizes,
downloadRequest.valetToken,
downloadRequest.url,
binaryPath,
)
const result = await downloader.run()
if (result === 'success') {
const mapping = await this.getFilesBackupsMappingFile()
mapping.files[uuid] = {
backedUpOn: new Date(),
absolutePath: fileDir,
relativePath: uuid,
metadataFileName: FileBackupsConstantsV1.MetadataFileName,
binaryFileName: FileBackupsConstantsV1.BinaryFileName,
version: FileBackupsConstantsV1.Version,
}
await this.saveFilesBackupsMappingFile(mapping)
}
return result
}
}

View File

@@ -0,0 +1,54 @@
import { WriteStream, createWriteStream } from 'fs'
import { downloadData } from './FileNetworking'
export class FileDownloader {
writeStream: WriteStream
constructor(private chunkSizes: number[], private valetToken: string, private url: string, filePath: string) {
this.writeStream = createWriteStream(filePath, { flags: 'a' })
}
public async run(): Promise<'success' | 'failed'> {
const result = await this.downloadChunk(0, 0)
this.writeStream.close()
return result
}
private async downloadChunk(chunkIndex = 0, contentRangeStart: number): Promise<'success' | 'failed'> {
const pullChunkSize = this.chunkSizes[chunkIndex]
const headers = {
'x-valet-token': this.valetToken,
'x-chunk-size': pullChunkSize.toString(),
range: `bytes=${contentRangeStart}-`,
}
const response = await downloadData(this.writeStream, this.url, headers)
if (!String(response.status).startsWith('2')) {
return 'failed'
}
const contentRangeHeader = response.headers['content-range'] as string
if (!contentRangeHeader) {
return 'failed'
}
const matches = contentRangeHeader.match(/(^[a-zA-Z][\w]*)\s+(\d+)\s?-\s?(\d+)?\s?\/?\s?(\d+|\*)?/)
if (!matches || matches.length !== 5) {
return 'failed'
}
const rangeStart = +matches[2]
const rangeEnd = +matches[3]
const totalSize = +matches[4]
if (rangeEnd < totalSize - 1) {
return this.downloadChunk(++chunkIndex, rangeStart + pullChunkSize)
}
return 'success'
}
}

View File

@@ -0,0 +1,22 @@
import { WriteStream } from 'fs'
import axios, { AxiosResponseHeaders, AxiosRequestHeaders } from 'axios'
export async function downloadData(
writeStream: WriteStream,
url: string,
headers: AxiosRequestHeaders,
): Promise<{
headers: AxiosResponseHeaders
status: number
}> {
const response = await axios.get(url, {
responseType: 'arraybuffer',
headers: headers,
})
if (String(response.status).startsWith('2')) {
writeStream.write(response.data)
}
return { headers: response.headers, status: response.status }
}