8000 [Backup] Bug fixes around backup cancellation and background by zatteo · Pull Request #1173 · cozy/cozy-flagship-app · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[Backup] Bug fixes around backup cancellation and background #1173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 20, 2024
22 changes: 22 additions & 0 deletions patches/react-native-fs+2.20.0+001+error-on-cancel.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
diff --git a/node_modules/react-native-fs/Uploader.m b/node_modules/react-native-fs/Uploader.m
index 9463c81..04c15cf 100644
--- a/node_modules/react-native-fs/Uploader.m
+++ b/node_modules/react-native-fs/Uploader.m
@@ -105,8 +105,15 @@ - (void)uploadFiles:(RNFSUploadParams*)params
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:(id)self delegateQueue:[NSOperationQueue mainQueue]];
_task = [session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
- NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
- return self->_params.completeCallback(str, response);
+ // The task was cancelled
+ if (error && [error.domain isEqual:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
+ NSError* cancelError = [NSError errorWithDomain:@"Uploader" code:NSURLErrorCancelled userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"Task was cancelled"]}];
+ return self->_params.errorCallback(cancelError);
+ }
+
+ // The task completed successfully or with an unknown error
+ NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ return self->_params.completeCallback(str, response);
}];
[_task resume];
[session finishTasksAndInvalidate];
24 changes: 12 additions & 12 deletions src/app/domain/backup/helpers/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { NetworkError, UploadError } from '/app/domain/upload/models'
import {
NetworkError,
CancellationError,
UploadError
} from '/app/domain/upload/models'

export class BackupError extends Error {
textMessage: string
Expand All @@ -24,6 +28,10 @@ export const isNetworkError = (error: unknown): boolean => {
)
}

export const isCancellationError = (error: unknown): boolean => {
return error instanceof CancellationError
}

export const isUploadError = (error: unknown): error is UploadError => {
return (
typeof error === 'object' &&
Expand All @@ -49,19 +57,11 @@ export const isFileTooBigError = (error: UploadError): boolean => {
)
}

export const isCancellationError = (error: UploadError): boolean => {
return (
error.statusCode === -1 &&
error.errors[0]?.detail === 'User cancelled upload'
)
}

export const shouldRetryCallbackBackup = (error: Error): boolean => {
const notRetryableError =
isUploadError(error) &&
(isQuotaExceededError(error) ||
isFileTooBigError(error) ||
isCancellationError(error))
(isUploadError(error) &&
(isQuotaExceededError(error) || isFileTooBigError(error))) ||
isCancellationError(error)

return !notRetryableError
}
26 changes: 21 additions & 5 deletions src/app/domain/backup/services/manageBackup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import {
import { prepareDeduplication } from '/app/domain/backup/services/manageRemoteBackupConfig'
import { prepareAlbums } from '/app/domain/backup/services/manageAlbums'
import { getMediasToBackup } from '/app/domain/backup/services/getMedias'
import { uploadMedias } from '/app/domain/backup/services/uploadMedias'
import {
uploadMedias,
setShouldStopBackup
} from '/app/domain/backup/services/uploadMedias'
StopBackupReason,
setStopBackupData,
addBackgroundSubscriptionListener
} from '/app/domain/backup/services/stopBackup'
import {
cancelUpload,
getCurrentUploadId
Expand Down Expand Up @@ -104,6 +106,10 @@ export const startBackup = async (

void onProgress(await getBackupInfo(client))

const backgroundSubscription = addBackgroundSubscriptionListener(
() => void stopBackup(client, 'STOPPED_BECAUSE_BACKGROUND')
)

try {
const partialSuccessMessage = await uploadMedias(
client,
Expand Down Expand Up @@ -186,6 +192,8 @@ export const startBackup = async (
redirectLink: 'photos/#/backup'
}
})
} finally {
backgroundSubscription.remove()
}

const localBackupConfigAfterUpload = await getLocalBackupConfig(client)
Expand All @@ -205,8 +213,16 @@ export const startBackup = async (
return await getBackupInfo(client)
}

export const stopBackup = async (client: CozyClient): Promise<BackupInfo> => {
setShouldStopBackup(true)
export const stopBackup = async (
client: CozyClient,
reason: StopBackupReason = 'STOPPED_BY_USER'
): Promise<BackupInfo> => {
log.debug('Stopping backup because', reason)

setStopBackupData({
shouldStop: true,
reason
})

const uploadId = getCurrentUploadId()

Expand Down
64 changes: 64 additions & 0 deletions src/app/domain/backup/services/stopBackup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AppState, NativeEventSubscription, Platform } from 'react-native'

import { t } from '/locales/i18n'

export type StopBackupReason = 'STOPPED_BY_USER' | 'STOPPED_BECAUSE_BACKGROUND'

interface StopBackupData {
shouldStop: boolean
reason: StopBackupReason | undefined
translatedReason: string
}

let stopBackupData: StopBackupData = {
shouldStop: false,
reason: undefined,
translatedReason: ''
}

const translateStopBackupReason = (
reason: StopBackupReason | undefined
): string => {
if (reason === 'STOPPED_BY_USER') {
return t('services.backup.errors.backupStopped')
} else if (reason === 'STOPPED_BECAUSE_BACKGROUND') {
return t('services.backup.errors.appKilled')
}

return ''
}

export const getStopBackupData = (): StopBackupData => {
return stopBackupData
}

export const setStopBackupData = (
newStopBackupData: Omit<StopBackupData, 'translatedReason'>
): void => {
stopBackupData = {
...newStopBackupData,
translatedReason: translateStopBackupReason(newStopBackupData.reason)
}
}

export const resetStopBackupData = (): void => {
stopBackupData = {
shouldStop: false,
reason: undefined,
translatedReason: ''
}
}

export const addBackgroundSubscriptionListener = (
callback: () => void
): NativeEventSubscription => {
return AppState.addEventListener('change', nextAppState => {
if (Platform.OS === 'android' && Platform.Version < 31) {
return
}

if (nextAppState === 'background') {
callback()
}
})
}
56 changes: 24 additions & 32 deletions src/app/domain/backup/services/uploadMedias.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
/* eslint-disable promise/always-return */

import { AppState, Platform } from 'react-native'

import { uploadMedia } from '/app/domain/backup/services/uploadMedia'
import {
setMediaAsBackupedBecauseUploaded,
Expand Down Expand Up @@ -30,6 +28,10 @@ import {
areAlbumsEnabled,
addMediaToAlbums
} from '/app/domain/backup/services/manageAlbums'
import {
getStopBackupData,
resetStopBackupData
} from '/app/domain/backup/services/stopBackup'
import { File } from '/app/domain/backup/queries'
import { t } from '/locales/i18n'

Expand All @@ -38,28 +40,11 @@ import type { IOCozyFile } from 'cozy-client'
import flag from 'cozy-flags'
import Minilog from 'cozy-minilog'

import { CancellationError } from '/app/domain/upload/models'
const log = Minilog('💿 Backup')

const DOCTYPE_FILES = 'io.cozy.files'

let shouldStopBackup = false

export const getShouldStopBackup = (): boolean => {
return shouldStopBackup
}

export const setShouldStopBackup = (value: boolean): void => {
shouldStopBackup = value
}

const shouldStopBecauseBackground = (): boolean => {
if (Platform.OS === 'android' && Platform.Version < 31) {
return false
}

return AppState.currentState === 'background'
}< F438 /td>

export const uploadMedias = async (
client: CozyClient,
localBackupConfig: LocalBackupConfig,
Expand All @@ -74,15 +59,11 @@ export const uploadMedias = async (
let firstPartialSuccessMessage: string | undefined

for (const mediaToUpload of mediasToUpload) {
if (shouldStopBackup) {
shouldStopBackup = false
log.debug('Backup stopped because asked by the user')
return t('services.backup.errors.backupStopped')
}

if (shouldStopBecauseBackground()) {
log.debug('Backup stopped because in background')
return t('services.backup.errors.appKilled')
const { shouldStop, reason, translatedReason } = getStopBackupData()
if (shouldStop) {
log.debug('Backup stopped at beginning of upload loop', reason)
resetStopBackupData()
return translatedReason
}

if (flag('flagship.backup.dedup')) {
Expand Down Expand Up @@ -127,6 +108,13 @@ export const uploadMedias = async (
throw new BackupError(t('services.backup.errors.networkIssue'))
}

if (isCancellationError(error)) {
const { reason, translatedReason } = getStopBackupData()
log.debug('Backup stopped because cancellation error', reason)
resetStopBackupData()
return translatedReason
}

if (isUploadError(error)) {
if (isQuotaExceededError(error)) {
throw new BackupError(
Expand All @@ -136,9 +124,6 @@ export const uploadMedias = async (
} else if (isFileTooBigError(error)) {
firstPartialSuccessMessage =
firstPartialSuccessMessage ?? t('services.backup.errors.fileTooBig')
} else if (isCancellationError(error)) {
shouldStopBackup = false
return t('services.backup.errors.backupStopped')
} else {
firstPartialSuccessMessage =
firstPartialSuccessMessage ??
Expand Down Expand Up @@ -179,6 +164,13 @@ const prepareAndUploadMedia = async (
uploadMetadata,
mediaToUpload
)

const { shouldStop } = getStopBackupData()
if (shouldStop) {
log.debug('Backup stopped before uploading media')
throw new CancellationError()
}

const { data: documentCreated } = await uploadMedia(
client,
uploadUrl,
Expand Down
7 changes: 7 additions & 0 deletions src/app/domain/upload/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,10 @@ export class NetworkError extends Error {
this.name = 'NetworkError'
}
}

export class CancellationError extends Error {
constructor() {
super()
this.name = 'CancellationError'
}
}
14 changes: 3 additions & 11 deletions src/app/domain/upload/services/upload.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { t } from '/locales/i18n'
import {
UploadParams,
UploadResult,
NetworkError
NetworkError,
CancellationError
} from '/app/domain/upload/models'

let currentUploadId: string | undefined
Expand Down Expand Up @@ -82,16 +83,7 @@ export const uploadFile = async ({
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
RNBackgroundUpload.addListener('cancelled', uploadId, data => {
return reject({
statusCode: -1,
errors: [
{
status: -1,
title: 'Upload cancelled',
detail: 'Upload cancelled'
}
]
})
return reject(new CancellationError())
})
RNBackgroundUpload.addListener('completed', uploadId, response => {
const { data } = JSON.parse(response.responseBody) as {
Expand Down
18 changes: 12 additions & 6 deletions src/app/domain/upload/services/upload.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { StackErrors, IOCozyFile } from 'cozy-client'
import {
UploadParams,
UploadResult,
NetworkError
NetworkError,
CancellationError
} from '/app/domain/upload/models'

let currentUploadId: string | undefined
Expand All @@ -27,7 +28,7 @@ export const uploadFile = async ({
mimetype
}: UploadParams): Promise<UploadResult> => {
return new Promise((resolve, reject) => {
RNFileSystem.uploadFiles({
const { jobId, promise } = RNFileSystem.uploadFiles({
toUrl: url,
files: [
{
Expand All @@ -47,12 +48,14 @@ export const uploadFile = async ({
Accept: 'application/json',
'Content-Type': mimetype,
Authorization: `Bearer ${token}`
},
begin: ({ jobId }) => {
setCurrentUploadId(jobId.toString())
}
})
.promise.then(response => {

setCurrentUploadId(jobId.toString())

// Start the upload
promise
.then(response => {
if (response.body === '' && !response.statusCode) {
return reject(new NetworkError())
}
Expand All @@ -75,6 +78,9 @@ export const uploadFile = async ({
}
})
.catch(e => {
if ((e as Error).message === 'Task was cancelled') {
return reject(new CancellationError())
}
return reject(e)
})
})
Expand Down
Loading
0