fix: downloading backups in mobile webview (#1703)
This commit is contained in:
@@ -28,7 +28,6 @@ import { hide, show } from 'react-native-privacy-snapshot'
|
||||
import Share from 'react-native-share'
|
||||
import { AppStateObserverService } from './../AppStateObserverService'
|
||||
import Keychain from './Keychain'
|
||||
import { SNReactNativeCrypto } from './ReactNativeCrypto'
|
||||
import { IsMobileWeb } from './Utils'
|
||||
|
||||
export type BiometricsType = 'Fingerprint' | 'Face ID' | 'Biometrics' | 'Touch ID'
|
||||
@@ -79,14 +78,11 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
platform: SNPlatform.Ios | SNPlatform.Android = Platform.OS === 'ios' ? SNPlatform.Ios : SNPlatform.Android
|
||||
private eventObservers: MobileDeviceEventHandler[] = []
|
||||
public isDarkMode = false
|
||||
private crypto: SNReactNativeCrypto
|
||||
|
||||
constructor(
|
||||
private stateObserverService?: AppStateObserverService,
|
||||
private androidBackHandlerService?: AndroidBackHandlerService,
|
||||
) {
|
||||
this.crypto = new SNReactNativeCrypto()
|
||||
}
|
||||
) {}
|
||||
|
||||
deinit() {
|
||||
this.stateObserverService?.deinit()
|
||||
@@ -541,8 +537,7 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
try {
|
||||
const path = this.getFileDestinationPath(filename, saveInTempLocation)
|
||||
void this.deleteFileAtPathIfExists(path)
|
||||
const decodedContents = this.crypto.base64Decode(base64.replace(/data.*base64,/, ''))
|
||||
await writeFile(path, decodedContents)
|
||||
await writeFile(path, base64.replace(/data.*base64,/, ''), 'base64')
|
||||
return path
|
||||
} catch (error) {
|
||||
this.consoleLog(`${error}`)
|
||||
|
||||
@@ -71,7 +71,7 @@ export class ArchiveManager {
|
||||
return string
|
||||
}
|
||||
|
||||
private async downloadZippedDecryptedItems(data: BackupFile) {
|
||||
async getZippedDecryptedItemsBlob(data: BackupFile) {
|
||||
const zip = await import('@zip.js/zip.js')
|
||||
const zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip'))
|
||||
const items = data.items
|
||||
@@ -83,8 +83,7 @@ export class ArchiveManager {
|
||||
const fileName = zippableFileName('Standard Notes Backup and Import File')
|
||||
await zipWriter.add(fileName, new zip.BlobReader(blob))
|
||||
|
||||
let index = 0
|
||||
const nextFile = async () => {
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const item = items[index]
|
||||
let name, contents
|
||||
|
||||
@@ -105,17 +104,14 @@ export class ArchiveManager {
|
||||
const fileName =
|
||||
`Items/${sanitizeFileName(item.content_type)}/` + zippableFileName(name, `-${item.uuid.split('-')[0]}`)
|
||||
await zipWriter.add(fileName, new zip.BlobReader(blob))
|
||||
|
||||
index++
|
||||
if (index < items.length) {
|
||||
await nextFile()
|
||||
} else {
|
||||
const finalBlob = await zipWriter.close()
|
||||
this.downloadData(finalBlob, `Standard Notes Backup - ${this.formattedDateForExports()}.zip`)
|
||||
}
|
||||
}
|
||||
|
||||
await nextFile()
|
||||
return await zipWriter.close()
|
||||
}
|
||||
|
||||
private async downloadZippedDecryptedItems(data: BackupFile) {
|
||||
const zippedDecryptedItemsBlob = await this.getZippedDecryptedItemsBlob(data)
|
||||
this.downloadData(zippedDecryptedItemsBlob, `Standard Notes Backup - ${this.formattedDateForExports()}.zip`)
|
||||
}
|
||||
|
||||
async zipData(data: ZippableData): Promise<Blob> {
|
||||
|
||||
@@ -16,8 +16,8 @@ import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
|
||||
import { shareSelectedItems } from '@/NativeMobileWeb/ShareSelectedItems'
|
||||
import { downloadSelectedItemsOnAndroid } from '@/NativeMobileWeb/DownloadSelectedItemsOnAndroid'
|
||||
import { shareSelectedNotes } from '@/NativeMobileWeb/ShareSelectedNotes'
|
||||
import { downloadSelectedNotesOnAndroid } from '@/NativeMobileWeb/DownloadSelectedNotesOnAndroid'
|
||||
|
||||
type DeletePermanentlyButtonProps = {
|
||||
onClick: () => void
|
||||
@@ -355,7 +355,7 @@ const NotesOptions = ({
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => {
|
||||
application.isNativeMobileWeb() ? shareSelectedItems(application, notes) : downloadSelectedItems()
|
||||
application.isNativeMobileWeb() ? shareSelectedNotes(application, notes) : downloadSelectedItems()
|
||||
}}
|
||||
>
|
||||
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
|
||||
@@ -364,7 +364,7 @@ const NotesOptions = ({
|
||||
{application.platform === Platform.Android && (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => downloadSelectedItemsOnAndroid(application, notes)}
|
||||
onClick={() => downloadSelectedNotesOnAndroid(application, notes)}
|
||||
>
|
||||
<Icon type="download" className={iconClass} />
|
||||
Export
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
import { alertDialog } from '@standardnotes/ui-services'
|
||||
import { alertDialog, sanitizeFileName } from '@standardnotes/ui-services'
|
||||
import {
|
||||
STRING_IMPORT_SUCCESS,
|
||||
STRING_INVALID_IMPORT_FILE,
|
||||
@@ -21,6 +21,7 @@ import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -59,8 +60,31 @@ const DataBackups = ({ application, viewControllerManager }: Props) => {
|
||||
refreshEncryptionStatus()
|
||||
}, [refreshEncryptionStatus])
|
||||
|
||||
const downloadDataArchive = () => {
|
||||
application.getArchiveService().downloadBackup(isBackupEncrypted).catch(console.error)
|
||||
const downloadDataArchive = async () => {
|
||||
const data = isBackupEncrypted
|
||||
? await application.createEncryptedBackupFile()
|
||||
: await application.createDecryptedBackupFile()
|
||||
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
const blobData = new Blob([JSON.stringify(data, null, 2)], {
|
||||
type: 'text/json',
|
||||
})
|
||||
|
||||
if (isBackupEncrypted) {
|
||||
const filename = `Standard Notes Encrypted Backup and Import File - ${application
|
||||
.getArchiveService()
|
||||
.formattedDateForExports()}`
|
||||
const sanitizedFilename = sanitizeFileName(filename) + '.txt'
|
||||
downloadOrShareBlobBasedOnPlatform(application, blobData, sanitizedFilename)
|
||||
} else {
|
||||
const zippedDecryptedItemsBlob = await application.getArchiveService().getZippedDecryptedItemsBlob(data)
|
||||
const filename = `Standard Notes Backup - ${application.getArchiveService().formattedDateForExports()}`
|
||||
const sanitizedFilename = sanitizeFileName(filename) + '.zip'
|
||||
downloadOrShareBlobBasedOnPlatform(application, zippedDecryptedItemsBlob, sanitizedFilename)
|
||||
}
|
||||
}
|
||||
|
||||
const readFile = async (file: File): Promise<any> => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { getBase64FromBlob } from '@/Utils'
|
||||
import { Platform } from '@standardnotes/snjs'
|
||||
import { addToast, ToastType, dismissToast } from '@standardnotes/toast'
|
||||
|
||||
export const downloadBlobOnAndroid = async (application: WebApplication, blob: Blob, filename: string) => {
|
||||
if (!application.isNativeMobileWeb() || application.platform !== Platform.Android) {
|
||||
throw new Error('Download function being used on non-android platform')
|
||||
}
|
||||
const loadingToastId = addToast({
|
||||
type: ToastType.Loading,
|
||||
message: `Downloading ${filename}..`,
|
||||
})
|
||||
const base64 = await getBase64FromBlob(blob)
|
||||
const downloaded = await application.mobileDevice.downloadBase64AsFile(base64, filename)
|
||||
if (downloaded) {
|
||||
dismissToast(loadingToastId)
|
||||
addToast({
|
||||
type: ToastType.Success,
|
||||
message: `Downloaded ${filename}`,
|
||||
})
|
||||
} else {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: `Could not download ${filename}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { getBase64FromBlob } from '@/Utils'
|
||||
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { Platform, SNNote } from '@standardnotes/snjs'
|
||||
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
||||
import { sanitizeFileName } from '@standardnotes/ui-services'
|
||||
|
||||
export const downloadSelectedItemsOnAndroid = async (application: WebApplication, notes: SNNote[]) => {
|
||||
if (!application.isNativeMobileWeb() || application.platform !== Platform.Android) {
|
||||
throw new Error('Function being used on non-android platform')
|
||||
}
|
||||
if (notes.length === 1) {
|
||||
const note = notes[0]
|
||||
const blob = getNoteBlob(application, note)
|
||||
const base64 = await getBase64FromBlob(blob)
|
||||
const { name, ext } = parseFileName(getNoteFileName(application, note))
|
||||
const filename = `${sanitizeFileName(name)}.${ext}`
|
||||
const loadingToastId = addToast({
|
||||
type: ToastType.Loading,
|
||||
message: `Exporting ${filename}..`,
|
||||
})
|
||||
const downloaded = await application.mobileDevice.downloadBase64AsFile(base64, filename)
|
||||
if (downloaded) {
|
||||
dismissToast(loadingToastId)
|
||||
addToast({
|
||||
type: ToastType.Success,
|
||||
message: `Exported ${filename}`,
|
||||
})
|
||||
} else {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: `Could not export ${filename}`,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (notes.length > 1) {
|
||||
const zippedDataBlob = await application.getArchiveService().zipData(
|
||||
notes.map((note) => {
|
||||
return {
|
||||
name: getNoteFileName(application, note),
|
||||
content: getNoteBlob(application, note),
|
||||
}
|
||||
}),
|
||||
)
|
||||
const zippedDataAsBase64 = await getBase64FromBlob(zippedDataBlob)
|
||||
const filename = `Standard Notes Export - ${application.getArchiveService().formattedDateForExports()}.zip`
|
||||
const loadingToastId = addToast({
|
||||
type: ToastType.Loading,
|
||||
message: `Exporting ${filename}..`,
|
||||
})
|
||||
const downloaded = await application.mobileDevice.downloadBase64AsFile(zippedDataAsBase64, filename)
|
||||
if (downloaded) {
|
||||
dismissToast(loadingToastId)
|
||||
addToast({
|
||||
type: ToastType.Success,
|
||||
message: `Exported ${filename}`,
|
||||
})
|
||||
} else {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: `Could not export ${filename}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { Platform, SNNote } from '@standardnotes/snjs'
|
||||
import { sanitizeFileName } from '@standardnotes/ui-services'
|
||||
import { downloadBlobOnAndroid } from './DownloadBlobOnAndroid'
|
||||
|
||||
export const downloadSelectedNotesOnAndroid = async (application: WebApplication, notes: SNNote[]) => {
|
||||
if (!application.isNativeMobileWeb() || application.platform !== Platform.Android) {
|
||||
throw new Error('Function being used on non-android platform')
|
||||
}
|
||||
if (notes.length === 1) {
|
||||
const note = notes[0]
|
||||
const blob = getNoteBlob(application, note)
|
||||
const { name, ext } = parseFileName(getNoteFileName(application, note))
|
||||
const filename = `${sanitizeFileName(name)}.${ext}`
|
||||
await downloadBlobOnAndroid(application, blob, filename)
|
||||
return
|
||||
}
|
||||
if (notes.length > 1) {
|
||||
const zippedDataBlob = await application.getArchiveService().zipData(
|
||||
notes.map((note) => {
|
||||
return {
|
||||
name: getNoteFileName(application, note),
|
||||
content: getNoteBlob(application, note),
|
||||
}
|
||||
}),
|
||||
)
|
||||
const filename = `Standard Notes Export - ${application.getArchiveService().formattedDateForExports()}.zip`
|
||||
await downloadBlobOnAndroid(application, zippedDataBlob, filename)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { getBase64FromBlob } from '@/Utils'
|
||||
|
||||
export const shareBlobOnMobile = async (application: WebApplication, blob: Blob, filename: string) => {
|
||||
if (!application.isNativeMobileWeb()) {
|
||||
throw new Error('Share function being used outside mobile webview')
|
||||
}
|
||||
const base64 = await getBase64FromBlob(blob)
|
||||
application.mobileDevice.shareBase64AsFile(base64, filename)
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { getBase64FromBlob } from '@/Utils'
|
||||
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import { sanitizeFileName } from '@standardnotes/ui-services'
|
||||
import { shareBlobOnMobile } from './ShareBlobOnMobile'
|
||||
|
||||
export const shareSelectedItems = async (application: WebApplication, notes: SNNote[]) => {
|
||||
export const shareSelectedNotes = async (application: WebApplication, notes: SNNote[]) => {
|
||||
if (!application.isNativeMobileWeb()) {
|
||||
throw new Error('Share function being used outside mobile webview')
|
||||
}
|
||||
if (notes.length === 1) {
|
||||
const note = notes[0]
|
||||
const blob = getNoteBlob(application, note)
|
||||
const base64 = await getBase64FromBlob(blob)
|
||||
const { name, ext } = parseFileName(getNoteFileName(application, note))
|
||||
const filename = `${sanitizeFileName(name)}.${ext}`
|
||||
application.mobileDevice.shareBase64AsFile(base64, filename)
|
||||
void shareBlobOnMobile(application, blob, filename)
|
||||
return
|
||||
}
|
||||
if (notes.length > 1) {
|
||||
@@ -27,9 +26,9 @@ export const shareSelectedItems = async (application: WebApplication, notes: SNN
|
||||
}
|
||||
}),
|
||||
)
|
||||
const zippedDataAsBase64 = await getBase64FromBlob(zippedDataBlob)
|
||||
application.mobileDevice.shareBase64AsFile(
|
||||
zippedDataAsBase64,
|
||||
void shareBlobOnMobile(
|
||||
application,
|
||||
zippedDataBlob,
|
||||
`Standard Notes Export - ${application.getArchiveService().formattedDateForExports()}.zip`,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { downloadBlobOnAndroid } from '@/NativeMobileWeb/DownloadBlobOnAndroid'
|
||||
import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile'
|
||||
import { Platform } from '@standardnotes/snjs'
|
||||
|
||||
export const downloadOrShareBlobBasedOnPlatform = async (application: WebApplication, blob: Blob, filename: string) => {
|
||||
if (!application.isNativeMobileWeb()) {
|
||||
application.getArchiveService().downloadData(blob, filename)
|
||||
return
|
||||
}
|
||||
|
||||
if (application.platform === Platform.Ios) {
|
||||
shareBlobOnMobile(application, blob, filename)
|
||||
return
|
||||
}
|
||||
|
||||
if (application.platform === Platform.Android) {
|
||||
downloadBlobOnAndroid(application, blob, filename)
|
||||
return
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user