Skip to content
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

Multiple bug fixed when deleting manually backup folder #1040

Merged
merged 11 commits into from
Dec 5, 2023
3 changes: 3 additions & 0 deletions src/@types/cozy-client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ declare module 'cozy-client' {
metadata?: {
backupDeviceIds: string[]
}
attributes: {
path: string
}
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/app/domain/backup/helpers/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UploadError } from '/app/domain/upload/models'
import { NetworkError, UploadError } from '/app/domain/upload/models'

export class BackupError extends Error {
textMessage: string
Expand All @@ -17,6 +17,13 @@ export class BackupError extends Error {
}
}

export const isNetworkError = (error: unknown): boolean => {
return (
error instanceof NetworkError ||
(error instanceof Error && error.message === 'Network request failed')
)
}

export const isUploadError = (error: unknown): error is UploadError => {
return (
typeof error === 'object' &&
Expand All @@ -26,6 +33,7 @@ export const isUploadError = (error: unknown): error is UploadError => {
Array.isArray(error.errors)
)
}

export const isQuotaExceededError = (error: UploadError): boolean => {
return (
error.statusCode === 413 &&
Expand Down
75 changes: 32 additions & 43 deletions src/app/domain/backup/services/manageBackup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import Minilog from 'cozy-minilog'

import {
getLocalBackupConfig,
initiazeLocalBackupConfig,
initializeLocalBackupConfig,
setBackupAsInitializing,
setBackupAsReady,
setBackupAsRunning,
setBackupAsDone,
setLastBackup,
updateRemoteBackupConfigLocally,
fixLocalBackupConfigIfNecessary,
addRemoteDuplicatesToBackupedMedias
} from '/app/domain/backup/services/manageLocalBackupConfig'
import { fetchBackupedAlbums } from '/app/domain/backup/services/manageAlbums'
import { getMediasToBackup } from '/app/domain/backup/services/getMedias'
import {
uploadMedias,
Expand All @@ -23,20 +22,13 @@ import {
cancelUpload,
getCurrentUploadId
} from '/app/domain/upload/services/upload'
import {
fetchDeviceRemoteBackupConfig,
fetchBackupedMedias,
createRemoteBackupFolder
} from '/app/domain/backup/services/manageRemoteBackupConfig'
import {
activateKeepAwake,
deactivateKeepAwake
} from '/app/domain/sleep/services/sleep'
import {
BackupInfo,
ProgressCallback,
BackupedMedia,
BackupedAlbum,
LocalBackupConfig,
LastBackup
} from '/app/domain/backup/models'
Expand Down Expand Up @@ -92,8 +84,6 @@ export const startBackup = async (
): Promise<BackupInfo> => {
log.debug('Backup started')

await updateRemoteBackupConfigLocally(client)

const localBackupConfig = await getLocalBackupConfig(client)

await setBackupAsRunning(client)
Expand All @@ -114,11 +104,13 @@ export const startBackup = async (
let status: LastBackup['status'] = 'success'
let titleKey = 'services.backup.notifications.backupSuccessTitle'
let bodyKey = 'services.backup.notifications.backupSuccessBody'
let message

if (partialSuccessMessage) {
status = 'partial_success'
titleKey = 'services.backup.notifications.backupPartialSuccessTitle'
bodyKey = 'services.backup.notifications.backupPartialSuccessBody'
message = partialSuccessMessage
}

await setLastBackup(client, {
Expand All @@ -127,7 +119,8 @@ export const startBackup = async (
postUploadLocalBackupConfig.currentBackup.totalMediasToBackupCount -
postUploadLocalBackupConfig.currentBackup.mediasToBackup.length,
totalMediasToBackupCount:
postUploadLocalBackupConfig.currentBackup.totalMediasToBackupCount
postUploadLocalBackupConfig.currentBackup.totalMediasToBackupCount,
message
})

await showLocalNotification({
Expand Down Expand Up @@ -228,43 +221,39 @@ export const getBackupInfo = async (
const initializeBackup = async (
client: CozyClient
): Promise<LocalBackupConfig> => {
try {
let backupConfig = await getLocalBackupConfig(client)
let localBackupConfig

log.debug('Backup found')

if (flag('flagship.backup.dedup')) {
await addRemoteDuplicatesToBackupedMedias(client)
backupConfig = await getLocalBackupConfig(client)
}

return backupConfig
try {
localBackupConfig = await getLocalBackupConfig(client)
} catch {
// if there is no local backup config
let deviceRemoteBackupConfig = await fetchDeviceRemoteBackupConfig(client)
let backupedMedias
let backupedAlbums
log.debug('Backup not found')

if (deviceRemoteBackupConfig) {
log.debug('Backup will be restored')
const newLocalBackupConfig = await initializeLocalBackupConfig(client)

backupedMedias = await fetchBackupedMedias(client)
backupedAlbums = await fetchBackupedAlbums(client)
} else {
log.debug('Backup will be created')
return newLocalBackupConfig
}

deviceRemoteBackupConfig = await createRemoteBackupFolder(client)
backupedMedias = [] as BackupedMedia[]
backupedAlbums = [] as BackupedAlbum[]
log.debug('Backup found')

try {
localBackupConfig = await fixLocalBackupConfigIfNecessary(client)
} catch (e) {
if (e instanceof Error) {
if (
e.message === 'Remote backup folder has been trashed.' ||
e.message === 'Remote backup folder has been deleted.'
) {
return await initializeLocalBackupConfig(client)
}
}

const backupConfig = await initiazeLocalBackupConfig(
client,
deviceRemoteBackupConfig,
backupedMedias,
backupedAlbums
)
throw e
}

return backupConfig
if (flag('flagship.backup.dedup')) {
await addRemoteDuplicatesToBackupedMedias(client)
localBackupConfig = await getLocalBackupConfig(client)
}

return localBackupConfig
}
71 changes: 57 additions & 14 deletions src/app/domain/backup/services/manageLocalBackupConfig.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import CozyClient, { FileCollectionGetResult, IOCozyFile } from 'cozy-client'
import CozyClient, {
FileCollectionGetResult,
IOCozyFile,
StackErrors
} from 'cozy-client'
import Minilog from 'cozy-minilog'

import {
Media,
LocalBackupConfig,
RemoteBackupConfig,
BackupedMedia,
BackupedAlbum,
LastBackup
} from '/app/domain/backup/models'
import { buildFileQuery } from '/app/domain/backup/queries'
import { fetchBackupedMedias } from '/app/domain/backup/services/manageRemoteBackupConfig'
import {
fetchBackupedMedias,
fetchDeviceRemoteBackupConfig,
createRemoteBackupFolder,
isInTrash
} from '/app/domain/backup/services/manageRemoteBackupConfig'
import { fetchBackupedAlbums } from '/app/domain/backup/services/manageAlbums'
import { isSameMedia } from '/app/domain/backup/helpers'
import {
getUserPersistedData,
Expand Down Expand Up @@ -66,15 +75,29 @@
)
}

export const initiazeLocalBackupConfig = async (
client: CozyClient,
remoteBackupConfig: RemoteBackupConfig,
backupedMedias: BackupedMedia[],
backupedAlbums: BackupedAlbum[]
export const initializeLocalBackupConfig = async (
client: CozyClient
): Promise<LocalBackupConfig> => {
let deviceRemoteBackupConfig = await fetchDeviceRemoteBackupConfig(client)
let backupedMedias
let backupedAlbums

if (deviceRemoteBackupConfig) {
log.debug('Backup will be restored')

backupedMedias = await fetchBackupedMedias(client)
backupedAlbums = await fetchBackupedAlbums(client)
} else {
log.debug('Backup will be created')

deviceRemoteBackupConfig = await createRemoteBackupFolder(client)
backupedMedias = [] as BackupedMedia[]
backupedAlbums = [] as BackupedAlbum[]
}

const newLocalBackupConfig = {
...INITIAL_BACKUP_CONFIG,
remoteBackupConfig: remoteBackupConfig,
remoteBackupConfig: deviceRemoteBackupConfig,
backupedMedias: backupedMedias,
backupedAlbums: backupedAlbums
}
Expand Down Expand Up @@ -119,7 +142,7 @@
uri: media.uri,
creationDate: media.creationDate,
modificationDate: media.modificationDate,
remoteId: documentCreated.id as string,

Check warning on line 145 in src/app/domain/backup/services/manageLocalBackupConfig.ts

View workflow job for this annotation

GitHub Actions / Quality Checks

Use a ! assertion to more succinctly remove null and undefined from the type
md5: documentCreated.attributes.md5sum
}

Expand All @@ -137,25 +160,45 @@
await setLocalBackupConfig(client, localBackupConfig)
}

export const updateRemoteBackupConfigLocally = async (
export const fixLocalBackupConfigIfNecessary = async (
client: CozyClient
): Promise<void> => {
): Promise<LocalBackupConfig> => {
const localBackupConfig = await getLocalBackupConfig(client)

const fileQuery = buildFileQuery(
localBackupConfig.remoteBackupConfig.backupFolder.id
)

const { data: remoteBackupFolderUpdated } = (await client.query(
fileQuery
)) as FileCollectionGetResult
let remoteBackupFolderUpdated

try {
const { data } = (await client.query(fileQuery)) as FileCollectionGetResult

remoteBackupFolderUpdated = data
} catch (e) {
if (e instanceof Error) {
const { errors } = JSON.parse(e.message) as StackErrors

if (errors.find(e => e.status === '404')) {
throw new Error('Remote backup folder has been deleted.')
}
}

throw e
}

if (isInTrash(remoteBackupFolderUpdated.attributes.path)) {
throw new Error('Remote backup folder has been trashed.')
}

localBackupConfig.remoteBackupConfig.backupFolder.name =
remoteBackupFolderUpdated.name
localBackupConfig.remoteBackupConfig.backupFolder.path =
remoteBackupFolderUpdated.path

await setLocalBackupConfig(client, localBackupConfig)

return localBackupConfig
}

export const setBackupAsToDo = async (client: CozyClient): Promise<void> => {
Expand Down
53 changes: 7 additions & 46 deletions src/app/domain/backup/services/manageRemoteBackupConfig.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as manageRemoteBackupConfig from '/app/domain/backup/services/manageRemoteBackupConfig'
import * as manageLocalBackupConfig from '/app/domain/backup/services/manageLocalBackupConfig'

import type CozyClient from 'cozy-client'

Expand All @@ -25,13 +24,14 @@ describe('fetchRemoteBackupConfigs', () => {
expect(remoteBackupConfigs).toEqual([])
})

test('returns an array with the correct backup folders when findReferencedBy returns an array with two items', async () => {
test('returns an array with backup folders sorted by most recent first when findReferencedBy returns an array with two items', async () => {
// Given
const client = mockClientWithFindReferencedByResult({
included: [
{
_id: '1',
attributes: {
created_at: '2023-11-28T12:00:31.541756+01:00',
name: 'Device 1',
path: '/Backup/Device 1',
metadata: { backupDeviceIds: ['A'] }
Expand All @@ -40,6 +40,7 @@ describe('fetchRemoteBackupConfigs', () => {
{
_id: '2',
attributes: {
created_at: '2023-11-28T12:10:00.402741+01:00',
name: 'Device 2',
path: '/Backup/Device 2',
metadata: { backupDeviceIds: ['B'] }
Expand All @@ -54,13 +55,13 @@ describe('fetchRemoteBackupConfigs', () => {

// Then
expect(remoteBackupConfigs).toEqual([
{
backupFolder: { id: '1', name: 'Device 1', path: '/Backup/Device 1' },
backupDeviceIds: ['A']
},
{
backupFolder: { id: '2', name: 'Device 2', path: '/Backup/Device 2' },
backupDeviceIds: ['B']
},
{
backupFolder: { id: '1', name: 'Device 1', path: '/Backup/Device 1' },
backupDeviceIds: ['A']
}
])
})
Expand Down Expand Up @@ -108,26 +109,6 @@ describe('fetchDeviceRemoteBackupConfig', () => {
backupDeviceIds: ['A']
}
])
jest
.spyOn(manageLocalBackupConfig, 'getLocalBackupConfig')
.mockResolvedValue({
remoteBackupConfig: {
backupFolder: {
id: '1',
name: 'Device 1',
path: '/Backup/Device 1'
},
backupDeviceIds: ['A']
},
lastBackupDate: 0,
backupedMedias: [],
backupedAlbums: [],
currentBackup: {
status: 'to_do',
mediasToBackup: [],
totalMediasToBackupCount: 0
}
})
jest
.spyOn(manageRemoteBackupConfig, 'isRemoteBackupConfigFromDevice')
.mockReturnValue(true)
Expand All @@ -149,26 +130,6 @@ describe('fetchDeviceRemoteBackupConfig', () => {
backupDeviceIds: ['A']
}
])
jest
.spyOn(manageLocalBackupConfig, 'getLocalBackupConfig')
.mockResolvedValue({
remoteBackupConfig: {
backupFolder: {
id: '1',
name: 'Device 1',
path: '/Backup/Device 1'
},
backupDeviceIds: ['A']
},
lastBackupDate: 0,
backupedMedias: [],
backupedAlbums: [],
currentBackup: {
status: 'to_do',
mediasToBackup: [],
totalMediasToBackupCount: 0
}
})
jest
.spyOn(manageRemoteBackupConfig, 'isRemoteBackupConfigFromDevice')
.mockReturnValue(false)
Expand Down
Loading
Loading