Skip to content

Commit

Permalink
feat: Support move for encrypted files
Browse files Browse the repository at this point in the history
This adds the possiilbity to move files from/to an encrypted folder.

3 scenarios are supported:
- From a non-encrypted folder to an encrypted folder
- From an encrypted folder to a non-encrypted folder
- From an encrypted folder to another encrypted folder

Note we do not support the moving of non-encrypted folder to an
encrypted one, because of the potential cost if it has a deep hierarchy
and/or many files. However, the moving of an encrypted folder to is
supported for both non-encrypted and encrypted folder, as the files remain
encrypted with the same encryption key.
  • Loading branch information
paultranvan committed May 27, 2022
1 parent 9ab58f9 commit bbefc0e
Show file tree
Hide file tree
Showing 15 changed files with 541 additions and 103 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

# 1.42.0

## ✨ Features

* Support moving files from/to encrypted folder

## 🐛 Bug Fixes

* Disable sharing on public file viewer
Expand Down
30 changes: 30 additions & 0 deletions jestHelpers/setup.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react'
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

Expand All @@ -10,6 +11,35 @@ jest.mock('cozy-ui/transpiled/react/utils/color', () => ({
getCssVariableValue: () => '#fff'
}))

jest.mock('cozy-keys-lib', () => ({
withVaultUnlockContext: BaseComponent => {
const Wrapper = props => {
return <BaseComponent {...props} />
}
Wrapper.displayName = `withVaultUnlockContext(${
BaseComponent.displayName || BaseComponent.name
})`
return Wrapper
},
withVaultClient: BaseComponent => {
const Component = props => (
<>
{({ vaultClient }) => (
<BaseComponent vaultClient={vaultClient} {...props} />
)}
</>
)

Component.displayName = `withVaultClient(${
BaseComponent.displayName || BaseComponent.name
})`

return Component
},
useVaultUnlockContext: jest.fn().mockReturnValue(jest.fn()),
useVaultClient: jest.fn()
}))

global.cozy = {
bar: {
BarLeft: () => null,
Expand Down
3 changes: 1 addition & 2 deletions src/drive/mobile/modules/upload/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,8 @@ export class DumbUpload extends Component {
>
<div>
<FileList
folder={folder}
files={data}
targets={items}
entries={items}
navigateTo={this.navigateTo}
/>
<LoadMore hasMore={hasMore} fetchMore={fetchMore} />
Expand Down
3 changes: 2 additions & 1 deletion src/drive/mobile/modules/upload/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { shallow } from 'enzyme'
import { DumbUpload, generateForQueue } from './'

jest.mock('cozy-keys-lib', () => ({
withVaultClient: jest.fn().mockReturnValue({})
withVaultClient: jest.fn().mockReturnValue({}),
withVaultUnlockContext: jest.fn().mockReturnValue({})
}))

const tSpy = jest.fn()
Expand Down
4 changes: 4 additions & 0 deletions src/drive/web/modules/actions/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { download } from './index'
import { DOCTYPE_FILES_ENCRYPTION } from 'drive/lib/doctypes'

jest.mock('cozy-keys-lib', () => ({
withVaultUnlockContext: jest.fn().mockReturnValue({})
}))

describe('download', () => {
it('should not display when an encrypted folder is selected', () => {
const files = [
Expand Down
16 changes: 8 additions & 8 deletions src/drive/web/modules/actions/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import {
getEncryptionKeyFromDirId,
downloadEncryptedFile,
decryptFile
decryptFileToBlob
} from 'drive/lib/encryption'
import { DOCTYPE_FILES_ENCRYPTION } from 'drive/lib/doctypes'
import { TRASH_DIR_ID } from 'drive/constants/config'
Expand Down Expand Up @@ -50,7 +50,7 @@ jest.mock('drive/lib/encryption', () => ({
...jest.requireActual('drive/lib/encryption'),
getEncryptionKeyFromDirId: jest.fn(),
downloadEncryptedFile: jest.fn(),
decryptFile: jest.fn()
decryptFileToBlob: jest.fn()
}))

describe('trashFiles', () => {
Expand Down Expand Up @@ -140,7 +140,7 @@ describe('downloadFiles', () => {
expect(downloadEncryptedFile).toHaveBeenCalledWith(
mockClient,
{},
{ file, encryptionKey: 'encryption-key' }
{ file, decryptionKey: 'encryption-key' }
)
})

Expand Down Expand Up @@ -254,14 +254,14 @@ describe('openFileWith', () => {

it('open an encrypted file', async () => {
getEncryptionKeyFromDirId.mockResolvedValueOnce('encryption-key')
decryptFile.mockResolvedValueOnce('fake file blob')
decryptFileToBlob.mockResolvedValueOnce('fake file blob')
await openFileWith(mockClient, encryptedFile, { vaultClient })
expect(decryptFile).toHaveBeenCalledWith(
expect(decryptFileToBlob).toHaveBeenCalledWith(
mockClient,
{},
{
file: encryptedFile,
encryptionKey: 'encryption-key'
decryptionKey: 'encryption-key'
}
)
expect(saveAndOpenWithCordova).toHaveBeenCalledWith('fake file blob', {
Expand Down Expand Up @@ -344,12 +344,12 @@ describe('exportFilesNative', () => {
await exportFilesNative(mockClient, encryptedFiles, { vaultClient })

encryptedFiles.forEach(file =>
expect(decryptFile).toHaveBeenCalledWith(
expect(decryptFileToBlob).toHaveBeenCalledWith(
mockClient,
{},
{
file,
encryptionKey: 'encryption-key'
decryptionKey: 'encryption-key'
}
)
)
Expand Down
3 changes: 2 additions & 1 deletion src/drive/web/modules/filelist/AddFolder.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ jest.mock('cozy-flags', () => jest.fn())
jest.mock('cozy-keys-lib', () => ({
withVaultClient: jest.fn().mockReturnValue({}),
useVaultClient: jest.fn(),
WebVaultClient: jest.fn().mockReturnValue({})
WebVaultClient: jest.fn().mockReturnValue({}),
withVaultUnlockContext: jest.fn().mockReturnValue({})
}))
describe('AddFolder', () => {
const setup = () => {
Expand Down
115 changes: 72 additions & 43 deletions src/drive/web/modules/move/FileList.jsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,86 @@
import React, { useState } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { DumbFile as File } from 'drive/web/modules/filelist/File'
import { VaultUnlocker } from 'cozy-keys-lib'
import { ROOT_DIR_ID } from 'drive/constants/config'
import { useVaultUnlockContext } from 'cozy-keys-lib'
import { isEncryptedFolder } from 'drive/lib/encryption'

const isInvalidMoveTarget = (subjects, target) => {
const isASubject = subjects.find(subject => subject._id === target._id)
const isAFile = target.type === 'file'
const getFoldersInEntries = entries => {
return entries.filter(entry => entry.type === 'directory')
}

const getEncryptedFolders = entries => {
return entries.filter(entry => {
if (entry.type !== 'directory') {
return false
}
return isEncryptedFolder(entry)
})
}

export const isInvalidMoveTarget = (entries, target) => {
const isTargetAnEntry = entries.find(subject => subject._id === target._id)
const isTargetAFile = target.type === 'file'
if (isTargetAFile || isTargetAnEntry) {
return true
}
const dirs = getFoldersInEntries(entries)
if (dirs.length > 0) {
const encryptedFoldersEntries = getEncryptedFolders(dirs)
const hasEncryptedFolderEntries = encryptedFoldersEntries.length > 0
const hasEncryptedAndNonEncryptedFolderEntries =
hasEncryptedFolderEntries &&
encryptedFoldersEntries.length !== dirs.length
const isTargetEncrypted = isEncryptedFolder(target)

return isAFile || isASubject
if (isTargetEncrypted && !hasEncryptedFolderEntries) {
// Do not allow moving a non-encrypted folder to an encrypted one
return true
}
if (isTargetEncrypted && hasEncryptedAndNonEncryptedFolderEntries) {
// Do not allow moving encrypted + non encrypted folders
return true
}
}
return false
}

const FileList = ({ targets, files, folder, navigateTo }) => {
const [shouldUnlock, setShouldUnlock] = useState(true)
const isEncFolder = isEncryptedFolder(folder)

if (isEncFolder && shouldUnlock) {
return (
<VaultUnlocker
onDismiss={() => {
setShouldUnlock(false)
return navigateTo(ROOT_DIR_ID)
}}
onUnlock={() => setShouldUnlock(false)}
/>
)
} else {
return (
<>
{files.map(file => (
<File
key={file.id}
disabled={isInvalidMoveTarget(targets, file)}
styleDisabled={isInvalidMoveTarget(targets, file)}
attributes={file}
displayedFolder={null}
actions={null}
isRenaming={false}
onFolderOpen={id => navigateTo(files.find(f => f.id === id))}
onFileOpen={null}
withSelectionCheckbox={false}
withFilePath={false}
withSharedBadge
/>
))}
</>
)
const FileList = ({ entries, files, folder, navigateTo }) => {
const { showUnlockForm } = useVaultUnlockContext()

const onFolderOpen = folderId => {
const dir = folder ? folder._id : files.find(f => f._id === folderId)
const shouldUnlock = isEncryptedFolder(dir)
if (shouldUnlock) {
return showUnlockForm({ onUnlock: () => navigateTo(dir) })
} else {
return navigateTo(dir)
}
}

return (
<>
{files.map(file => (
<File
key={file.id}
disabled={isInvalidMoveTarget(entries, file)}
styleDisabled={isInvalidMoveTarget(entries, file)}
attributes={file}
displayedFolder={null}
actions={null}
isRenaming={false}
onFolderOpen={id => onFolderOpen(id)}
onFileOpen={null}
withSelectionCheckbox={false}
withFilePath={false}
withSharedBadge
/>
))}
</>
)
}

FileList.propTypes = {
targets: PropTypes.array.isRequired,
entries: PropTypes.array.isRequired,
files: PropTypes.array.isRequired,
navigateTo: PropTypes.func.isRequired,
folder: PropTypes.object
Expand Down
Loading

0 comments on commit bbefc0e

Please sign in to comment.