Skip to content

Commit

Permalink
Merge pull request #11000 from owncloud/feat/restore-web-worker
Browse files Browse the repository at this point in the history
feat: web worker for resource restore
  • Loading branch information
JammingBen authored Jun 13, 2024
2 parents c78bdbb + af540b3 commit aff8bd2
Show file tree
Hide file tree
Showing 11 changed files with 511 additions and 174 deletions.
20 changes: 20 additions & 0 deletions packages/web-client/src/helpers/httpError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HttpError } from '../errors'

/**
* Create a HttpError based on a given message, status code and x-request-id.
*/
export const createHttpError = ({
message,
statusCode,
xReqId
}: {
message: string
statusCode: number
xReqId: string
}) => {
const response = new Response(undefined, {
headers: { 'x-request-id': xReqId },
status: statusCode
})
return new HttpError(message, response, statusCode)
}
1 change: 1 addition & 0 deletions packages/web-client/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './call'
export * from './httpError'
export * from './item'
export * from './publicLink'
export * from './resource'
Expand Down
154 changes: 52 additions & 102 deletions packages/web-pkg/src/composables/actions/files/useFileActionsRestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,24 @@ import {
} from '../../../helpers/resource'
import { urlJoin } from '@ownclouders/web-client'
import { useClientService } from '../../clientService'
import { useLoadingService } from '../../loadingService'
import { useRouter } from '../../router'
import { computed } from 'vue'
import { computed, unref } from 'vue'
import { useGettext } from 'vue3-gettext'
import { FileAction, FileActionOptions } from '../types'
import { LoadingTaskCallbackArguments } from '../../../services'
import type { FileAction, FileActionOptions } from '../types'
import { useMessages, useSpacesStore, useUserStore, useResourcesStore } from '../../piniaStores'
import { useRestoreWorker } from '../../webWorkers/restoreWorker'

export const useFileActionsRestore = () => {
const { showMessage, showErrorMessage } = useMessages()
const userStore = useUserStore()
const router = useRouter()
const { $gettext, $ngettext } = useGettext()
const clientService = useClientService()
const loadingService = useLoadingService()
const spacesStore = useSpacesStore()
const resourcesStore = useResourcesStore()
const { startWorker } = useRestoreWorker()

// FIXME: use ConflictDialog class for this
const collectConflicts = async (space: SpaceResource, sortedResources: Resource[]) => {
const existingResourcesCache: Record<string, Resource[]> = {}
const conflicts: Resource[] = []
Expand Down Expand Up @@ -74,12 +74,13 @@ export const useFileActionsRestore = () => {
}
}

// FIXME: use ConflictDialog class for this
const collectResolveStrategies = async (conflicts: Resource[]) => {
let count = 0
const resolvedConflicts = []
const allConflictsCount = conflicts.length
let doForAllConflicts = false
let allConflictsStrategy
let allConflictsStrategy: ResolveStrategy
for (const conflict of conflicts) {
const isFolder = conflict.type === 'folder'
if (doForAllConflicts) {
Expand Down Expand Up @@ -109,105 +110,58 @@ export const useFileActionsRestore = () => {
return resolvedConflicts
}

const createFolderStructure = async (
space: SpaceResource,
path: string,
existingPaths: string[]
) => {
const { webdav } = clientService

const pathSegments = path.split('/').filter(Boolean)
let parentPath = ''
for (const subFolder of pathSegments) {
const folderPath = urlJoin(parentPath, subFolder)
if (existingPaths.includes(folderPath)) {
parentPath = urlJoin(parentPath, subFolder)
continue
}

try {
await webdav.createFolder(space, { path: folderPath })
} catch (ignored) {}

existingPaths.push(folderPath)
parentPath = folderPath
}

return {
existingPaths
}
}

const restoreResources = async (
const restoreResources = (
space: SpaceResource,
resources: Resource[],
missingFolderPaths: string[],
{ setProgress }: LoadingTaskCallbackArguments
missingFolderPaths: string[]
) => {
const restoredResources: Resource[] = []
const failedResources: Resource[] = []
const errors: Error[] = []

let createdFolderPaths: string[] = []
for (const [i, resource] of resources.entries()) {
const parentPath = dirname(resource.path)
if (missingFolderPaths.includes(parentPath)) {
const { existingPaths } = await createFolderStructure(space, parentPath, createdFolderPaths)
createdFolderPaths = existingPaths
}
const originalRoute = unref(router.currentRoute)

startWorker({ space, resources, missingFolderPaths }, async ({ successful, failed }) => {
if (successful.length) {
let title: string
if (successful.length === 1) {
title = $gettext('%{resource} was restored successfully', {
resource: successful[0].name
})
} else {
title = $gettext('%{resourceCount} files restored successfully', {
resourceCount: successful.length.toString()
})
}
showMessage({ title })

try {
await clientService.webdav.restoreFile(space, resource, resource, {
overwrite: true
})
restoredResources.push(resource)
} catch (e) {
console.error(e)
errors.push(e)
failedResources.push(resource)
} finally {
setProgress({ total: resources.length, current: i + 1 })
}
}
// user hasn't navigated to another location meanwhile
if (
originalRoute.name === unref(router.currentRoute).name &&
originalRoute.query?.fileId === unref(router.currentRoute).query?.fileId
) {
resourcesStore.removeResources(successful)
resourcesStore.resetSelection()
}

// success handler (for partial and full success)
if (restoredResources.length) {
resourcesStore.removeResources(restoredResources)
resourcesStore.resetSelection()
let title: string
if (restoredResources.length === 1) {
title = $gettext('%{resource} was restored successfully', {
resource: restoredResources[0].name
})
} else {
title = $gettext('%{resourceCount} files restored successfully', {
resourceCount: restoredResources.length.toString()
// Reload quota
const graphClient = clientService.graphAuthenticated
const driveResponse = await graphClient.drives.getDrive(space.id)
spacesStore.updateSpaceField({
id: driveResponse.data.id,
field: 'spaceQuota',
value: driveResponse.data.quota
})
}
showMessage({ title })
}

// failure handler (for partial and full failure)
if (failedResources.length) {
let translated
const translateParams: Record<string, string> = {}
if (failedResources.length === 1) {
translateParams.resource = failedResources[0].name
translated = $gettext('Failed to restore "%{resource}"', translateParams, true)
} else {
translateParams.resourceCount = failedResources.length.toString()
translated = $gettext('Failed to restore %{resourceCount} files', translateParams, true)
if (failed.length) {
let translated: string
const translateParams: Record<string, string> = {}
if (failed.length === 1) {
translateParams.resource = failed[0].resource.name
translated = $gettext('Failed to restore "%{resource}"', translateParams, true)
} else {
translateParams.resourceCount = failed.length.toString()
translated = $gettext('Failed to restore %{resourceCount} files', translateParams, true)
}
showErrorMessage({ title: translated, errors: failed.map(({ error }) => error) })
}
showErrorMessage({ title: translated, errors })
}

// Reload quota
const graphClient = clientService.graphAuthenticated
const driveResponse = await graphClient.drives.getDrive(space.id)
spacesStore.updateSpaceField({
id: driveResponse.data.id,
field: 'spaceQuota',
value: driveResponse.data.quota
})
}

Expand Down Expand Up @@ -245,12 +199,8 @@ export const useFileActionsRestore = () => {
resource.path = urlJoin(parentPath, resolvedName)
resolvedResources.push(resource)
}
return loadingService.addTask(
({ setProgress }) => {
return restoreResources(space, resolvedResources, missingFolderPaths, { setProgress })
},
{ indeterminate: false }
)

return restoreResources(space, resolvedResources, missingFolderPaths)
}

const actions = computed((): FileAction[] => [
Expand Down
26 changes: 6 additions & 20 deletions packages/web-pkg/src/composables/piniaStores/messages.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { defineStore } from 'pinia'
import { v4 as uuidV4 } from 'uuid'
import { ref, unref } from 'vue'
import { HttpError } from '@ownclouders/web-client'

type MessageError = Error | HttpError

export interface Message {
id: string
title: string
desc?: string
errors?: Error[]
errors?: MessageError[]
errorLogContent?: string
timeout?: number
status?: string
Expand All @@ -22,25 +25,8 @@ export const useMessages = defineStore('messages', () => {
}

const getXRequestIdsFromErrors = (errors: Message['errors']) => {
const getXRequestID = (error: any): string | null => {
/**
* x-request-id response headers might be very nested in ownCloud SDK,
* only remove records if you are sure they aren't valid anymore.
* FIXME: remove/simplify as soon as SDK has been removed
*/
if (error.response?.res?.res?.headers?.['x-request-id']) {
return error.response.res.res.headers['x-request-id']
}
if (error.response?.headers?.map?.['x-request-id']) {
return error.response.headers.map['x-request-id']
}
if (error.response?.res?.headers?.['x-request-id']) {
return error.response.res.headers['x-request-id']
}
if (error.response?.headers?.['x-request-id']) {
return error.response.headers['x-request-id']
}
return null
const getXRequestID = (error: MessageError) => {
return (error as HttpError).response?.headers?.get('x-request-id')
}

return errors
Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/webWorkers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './deleteWorker'
export * from './pasteWorker'
export * from './restoreWorker'
export * from './tokenTimerWorker'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useRestoreWorker'
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { unref } from 'vue'
import { useWebWorkersStore } from '../../piniaStores/webWorkers'
import {
HttpError,
createHttpError,
type Resource,
type SpaceResource
} from '@ownclouders/web-client'
import { useConfigStore } from '../../piniaStores'
import { useLoadingService } from '../../loadingService'
import { useRequestHeaders } from '../../requestHeaders'
import RestoreWorker from './worker?worker'

export type RestoreWorkerReturnData = {
successful: Resource[]
failed: { resource: Resource; message: string; statusCode: number; xReqId: string }[]
}

type CallbackOptions = {
successful: Resource[]
failed: { resource: Resource; error: HttpError }[]
}

/**
* Web worker for restoring deleted resources from the trash bin.
*/
export const useRestoreWorker = () => {
const configStore = useConfigStore()
const loadingService = useLoadingService()
const { headers } = useRequestHeaders()
const { createWorker, terminateWorker } = useWebWorkersStore()

const startWorker = (
{
space,
resources,
missingFolderPaths
}: { space: SpaceResource; resources: Resource[]; missingFolderPaths: string[] },
callback: (result: CallbackOptions) => void
) => {
const worker = createWorker<RestoreWorkerReturnData>(RestoreWorker as unknown as string, {
needsTokenRenewal: true
})

let resolveLoading: (value: unknown) => void

unref(worker.worker).onmessage = (e: MessageEvent) => {
terminateWorker(worker.id)
resolveLoading?.(true)

const { successful, failed } = JSON.parse(e.data) as RestoreWorkerReturnData

// construct http error based on the parsed error data
const failedWithErrors = failed.map(({ resource, ...errorData }) => {
return { resource, error: createHttpError(errorData) }
})

callback({ successful, failed: failedWithErrors })
}

loadingService.addTask(
() =>
new Promise((res) => {
resolveLoading = res
})
)

worker.post(getWorkerData({ space, resources, missingFolderPaths }))
}

const getWorkerData = ({
space,
resources,
missingFolderPaths
}: {
space: SpaceResource
resources: Resource[]
missingFolderPaths: string[]
}) => {
return JSON.stringify({
topic: 'startProcess',
data: {
space,
resources,
missingFolderPaths,
baseUrl: configStore.serverUrl,
headers: unref(headers)
}
})
}

return { startWorker }
}
Loading

0 comments on commit aff8bd2

Please sign in to comment.