Skip to content

Commit 4dfccad

Browse files
committed
feat: Support move for encrypted files
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.
1 parent 9cfd552 commit 4dfccad

File tree

6 files changed

+487
-72
lines changed

6 files changed

+487
-72
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# 1.42.0
22

3+
## ✨ Features
4+
5+
* Support moving files from/to encrypted folder
6+
37
## 🐛 Bug Fixes
48

59
* Disable sharing on public file viewer

src/drive/mobile/modules/upload/index.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,8 @@ export class DumbUpload extends Component {
135135
>
136136
<div>
137137
<FileList
138-
folder={folder}
139138
files={data}
140-
targets={items}
139+
entries={items}
141140
navigateTo={this.navigateTo}
142141
/>
143142
<LoadMore hasMore={hasMore} fetchMore={fetchMore} />

src/drive/web/modules/move/FileList.jsx

Lines changed: 72 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,86 @@
1-
import React, { useState } from 'react'
1+
import React from 'react'
22
import PropTypes from 'prop-types'
33
import { DumbFile as File } from 'drive/web/modules/filelist/File'
4-
import { VaultUnlocker } from 'cozy-keys-lib'
5-
import { ROOT_DIR_ID } from 'drive/constants/config'
4+
import { useVaultUnlockContext } from 'cozy-keys-lib'
65
import { isEncryptedFolder } from 'drive/lib/encryption'
76

8-
const isInvalidMoveTarget = (subjects, target) => {
9-
const isASubject = subjects.find(subject => subject._id === target._id)
10-
const isAFile = target.type === 'file'
7+
const getFoldersInEntries = entries => {
8+
return entries.filter(entry => entry.type === 'directory')
9+
}
10+
11+
const getEncryptedFolders = entries => {
12+
return entries.filter(entry => {
13+
if (entry.type !== 'directory') {
14+
return false
15+
}
16+
return isEncryptedFolder(entry)
17+
})
18+
}
19+
20+
export const isInvalidMoveTarget = (entries, target) => {
21+
const isTargetAnEntry = entries.find(subject => subject._id === target._id)
22+
const isTargetAFile = target.type === 'file'
23+
if (isTargetAFile || isTargetAnEntry) {
24+
return true
25+
}
26+
const dirs = getFoldersInEntries(entries)
27+
if (dirs.length > 0) {
28+
const encryptedFoldersEntries = getEncryptedFolders(dirs)
29+
const hasEncryptedFolderEntries = encryptedFoldersEntries.length > 0
30+
const hasEncryptedAndNonEncryptedFolderEntries =
31+
hasEncryptedFolderEntries &&
32+
encryptedFoldersEntries.length !== dirs.length
33+
const isTargetEncrypted = isEncryptedFolder(target)
1134

12-
return isAFile || isASubject
35+
if (isTargetEncrypted && !hasEncryptedFolderEntries) {
36+
// Do not allow moving a non-encrypted folder to an encrypted one
37+
return true
38+
}
39+
if (isTargetEncrypted && hasEncryptedAndNonEncryptedFolderEntries) {
40+
// Do not allow moving encrypted + non encrypted folders
41+
return true
42+
}
43+
}
44+
return false
1345
}
1446

15-
const FileList = ({ targets, files, folder, navigateTo }) => {
16-
const [shouldUnlock, setShouldUnlock] = useState(true)
17-
const isEncFolder = isEncryptedFolder(folder)
18-
19-
if (isEncFolder && shouldUnlock) {
20-
return (
21-
<VaultUnlocker
22-
onDismiss={() => {
23-
setShouldUnlock(false)
24-
return navigateTo(ROOT_DIR_ID)
25-
}}
26-
onUnlock={() => setShouldUnlock(false)}
27-
/>
28-
)
29-
} else {
30-
return (
31-
<>
32-
{files.map(file => (
33-
<File
34-
key={file.id}
35-
disabled={isInvalidMoveTarget(targets, file)}
36-
styleDisabled={isInvalidMoveTarget(targets, file)}
37-
attributes={file}
38-
displayedFolder={null}
39-
actions={null}
40-
isRenaming={false}
41-
onFolderOpen={id => navigateTo(files.find(f => f.id === id))}
42-
onFileOpen={null}
43-
withSelectionCheckbox={false}
44-
withFilePath={false}
45-
withSharedBadge
46-
/>
47-
))}
48-
</>
49-
)
47+
const FileList = ({ entries, files, folder, navigateTo }) => {
48+
const { showUnlockForm } = useVaultUnlockContext()
49+
50+
const onFolderOpen = folderId => {
51+
const dir = folder ? folder._id : files.find(f => f._id === folderId)
52+
const shouldUnlock = isEncryptedFolder(dir)
53+
if (shouldUnlock) {
54+
return showUnlockForm({ onUnlock: () => navigateTo(dir) })
55+
} else {
56+
return navigateTo(dir)
57+
}
5058
}
59+
60+
return (
61+
<>
62+
{files.map(file => (
63+
<File
64+
key={file.id}
65+
disabled={isInvalidMoveTarget(entries, file)}
66+
styleDisabled={isInvalidMoveTarget(entries, file)}
67+
attributes={file}
68+
displayedFolder={null}
69+
actions={null}
70+
isRenaming={false}
71+
onFolderOpen={id => onFolderOpen(id)}
72+
onFileOpen={null}
73+
withSelectionCheckbox={false}
74+
withFilePath={false}
75+
withSharedBadge
76+
/>
77+
))}
78+
</>
79+
)
5180
}
5281

5382
FileList.propTypes = {
54-
targets: PropTypes.array.isRequired,
83+
entries: PropTypes.array.isRequired,
5584
files: PropTypes.array.isRequired,
5685
navigateTo: PropTypes.func.isRequired,
5786
folder: PropTypes.object
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React from 'react'
2+
import { render } from '@testing-library/react'
3+
import { createMockClient } from 'cozy-client'
4+
import { DOCTYPE_FILES_ENCRYPTION } from 'drive/lib/doctypes'
5+
import AppLike from 'test/components/AppLike'
6+
import { generateFile } from 'test/generate'
7+
import FileList, { isInvalidMoveTarget } from 'drive/web/modules/move/FileList'
8+
9+
jest.mock('cozy-keys-lib', () => ({
10+
useVaultUnlockContext: jest
11+
.fn()
12+
.mockReturnValue({ showUnlockForm: jest.fn() })
13+
}))
14+
const client = createMockClient({})
15+
16+
const setup = ({ files, entries, navigateTo }) => {
17+
const root = render(
18+
<AppLike client={client}>
19+
<FileList files={files} entries={entries} navigateTo={navigateTo} />
20+
</AppLike>
21+
)
22+
23+
return { root }
24+
}
25+
26+
describe('FileList', () => {
27+
it('should display files', () => {
28+
const entries = [generateFile({ i: '1', type: 'file' })]
29+
const files = [
30+
generateFile({ i: '1', type: 'directory', prefix: '' }),
31+
generateFile({ i: '2', type: 'file', prefix: '' })
32+
]
33+
34+
const { root } = setup({ files, entries, navigateTo: () => null })
35+
const { queryAllByText } = root
36+
37+
expect(queryAllByText('directory-1')).toBeTruthy()
38+
expect(queryAllByText('file-2')).toBeTruthy()
39+
})
40+
})
41+
42+
describe('isInvalidMoveTarget', () => {
43+
it('should return true when target is a file', () => {
44+
const target = { _id: '1', type: 'file' }
45+
const entries = [{ _id: 'dir1', type: 'directory' }]
46+
expect(isInvalidMoveTarget(entries, target)).toBe(true)
47+
})
48+
49+
it('should return true when subject is the target', () => {
50+
const target = { _id: '1', type: 'directory' }
51+
const entries = [{ _id: '1', type: 'directory' }]
52+
expect(isInvalidMoveTarget(entries, target)).toBe(true)
53+
})
54+
55+
it('should return true when target is an encrypted directory and entries has non-encrypted folder', () => {
56+
const target = {
57+
_id: '1',
58+
type: 'directory',
59+
referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }]
60+
}
61+
const entries = [{ _id: 'dir1', type: 'directory' }]
62+
expect(isInvalidMoveTarget(entries, target)).toBe(true)
63+
})
64+
65+
it('shold return true when target is an encrypted dir and entries include both encrytped and non-encrypted dir', () => {
66+
const target = {
67+
_id: '1',
68+
type: 'directory',
69+
referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }]
70+
}
71+
const entries = [
72+
{
73+
_id: 'dir1',
74+
type: 'directory',
75+
referenced_by: [
76+
{ type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }
77+
]
78+
},
79+
{
80+
_id: 'dir2',
81+
type: 'directory'
82+
}
83+
]
84+
expect(isInvalidMoveTarget(entries, target)).toBe(true)
85+
})
86+
87+
it('should return false when both target and entries are encrypted', () => {
88+
const target = {
89+
_id: '1',
90+
type: 'directory',
91+
referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }]
92+
}
93+
const entries = [
94+
{
95+
_id: 'dir1',
96+
type: 'directory',
97+
referenced_by: [
98+
{ type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }
99+
]
100+
}
101+
]
102+
expect(isInvalidMoveTarget(entries, target)).toBe(false)
103+
})
104+
105+
it('should return false when both target and entries are regular folders', () => {
106+
const target = { _id: '1', type: 'directory' }
107+
const entries = [{ _id: 'dir1', type: 'directory' }]
108+
expect(isInvalidMoveTarget(entries, target)).toBe(false)
109+
})
110+
111+
it('should return false when subject is a mix of regular file and folder and target a regular folder', () => {
112+
const target = { _id: '1', type: 'directory' }
113+
const entries = [
114+
{ _id: 'dir1', type: 'file' },
115+
{ _id: 'file1', type: 'file' }
116+
]
117+
expect(isInvalidMoveTarget(entries, target)).toBe(false)
118+
})
119+
})

0 commit comments

Comments
 (0)