diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b15667ece..78d0e073f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@
# 1.42.0
+## ✨ Features
+
+* Support moving files from/to encrypted folder
+
## 🐛 Bug Fixes
* Disable sharing on public file viewer
diff --git a/jestHelpers/setup.js b/jestHelpers/setup.js
index 0c4111ad62..5d3a8c6d3d 100644
--- a/jestHelpers/setup.js
+++ b/jestHelpers/setup.js
@@ -1,3 +1,4 @@
+import React from 'react'
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
@@ -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
+ }
+ Wrapper.displayName = `withVaultUnlockContext(${
+ BaseComponent.displayName || BaseComponent.name
+ })`
+ return Wrapper
+ },
+ withVaultClient: BaseComponent => {
+ const Component = props => (
+ <>
+ {({ vaultClient }) => (
+
+ )}
+ >
+ )
+
+ Component.displayName = `withVaultClient(${
+ BaseComponent.displayName || BaseComponent.name
+ })`
+
+ return Component
+ },
+ useVaultUnlockContext: jest.fn().mockReturnValue(jest.fn()),
+ useVaultClient: jest.fn()
+}))
+
global.cozy = {
bar: {
BarLeft: () => null,
diff --git a/package.json b/package.json
index b50855a1a5..4d4a750aa0 100644
--- a/package.json
+++ b/package.json
@@ -130,7 +130,7 @@
"cozy-scanner": "2.0.2",
"cozy-scripts": "6.3.1",
"cozy-sharing": "4.1.5",
- "cozy-stack-client": "^29.0.0",
+ "cozy-stack-client": "^29.1.0",
"cozy-ui": "^67.0.2",
"date-fns": "1.30.1",
"diacritics": "1.3.0",
diff --git a/src/drive/lib/encryption.js b/src/drive/lib/encryption.js
index 7a3c110c1c..fbf77f4584 100644
--- a/src/drive/lib/encryption.js
+++ b/src/drive/lib/encryption.js
@@ -60,10 +60,10 @@ export const createEncryptedDir = async (
export const encryptAndUploadNewFile = async (
client,
vaultClient,
- { file, encryptionKey, fileOptions }
+ { binary, encryptionKey, fileOptions }
) => {
const { name, dirID, onUploadProgress } = fileOptions
- const encryptedFile = await vaultClient.encryptFile(file, encryptionKey)
+ const encryptedFile = await vaultClient.encryptFile(binary, encryptionKey)
const resp = await client
.collection(DOCTYPE_FILES)
.createFile(encryptedFile, {
@@ -75,6 +75,69 @@ export const encryptAndUploadNewFile = async (
return resp.data
}
+export const encryptAndUploadExistingFile = async (
+ client,
+ vaultClient,
+ { file, encryptionKey }
+) => {
+ const encryptedFile = await encryptFile(client, vaultClient, {
+ file,
+ encryptionKey
+ })
+
+ const attributes = {
+ ...file,
+ encrypted: true,
+ fileId: file._id,
+ data: encryptedFile
+ }
+ const resp = await client.save(attributes)
+ return resp.data
+}
+
+export const decryptAndUploadExistingFile = async (
+ client,
+ vaultClient,
+ { file, decryptionKey }
+) => {
+ const plaintextFile = await decryptFile(client, vaultClient, {
+ file,
+ decryptionKey
+ })
+
+ const attributes = {
+ ...file,
+ encrypted: false,
+ fileId: file._id,
+ data: plaintextFile
+ }
+ const resp = await client.save(attributes)
+ return resp.data
+}
+
+export const reencryptAndUploadExistingFile = async (
+ client,
+ vaultClient,
+ { file, encryptionKey, decryptionKey }
+) => {
+ const plaintextFile = await decryptFile(client, vaultClient, {
+ file,
+ decryptionKey
+ })
+ const reencryptedFile = await vaultClient.encryptFile(
+ plaintextFile,
+ encryptionKey
+ )
+ const attributes = {
+ ...file,
+ encrypted: true,
+ fileId: file._id,
+ data: reencryptedFile
+ }
+ const resp = await client.save(attributes)
+ return resp.data
+}
+
const getBinaryFile = async (client, fileId) => {
const resp = await client
.collection(DOCTYPE_FILES)
@@ -82,24 +145,36 @@ const getBinaryFile = async (client, fileId) => {
return resp.arrayBuffer()
}
-export const decryptFile = async (
+const decryptFile = async (client, vaultClient, { file, decryptionKey }) => {
+ const cipher = await getBinaryFile(client, file._id)
+ return vaultClient.decryptFile(cipher, decryptionKey)
+}
+
+const encryptFile = async (client, vaultClient, { file, encryptionKey }) => {
+ const cipher = await getBinaryFile(client, file._id)
+ return vaultClient.encryptFile(cipher, encryptionKey)
+}
+
+export const decryptFileIntoBlob = async (
client,
vaultClient,
- { file, encryptionKey }
+ { file, decryptionKey }
) => {
- const cipher = await getBinaryFile(client, file._id)
- const decryptedFile = await vaultClient.decryptFile(cipher, encryptionKey)
- return new Blob([decryptedFile], { type: file.type })
+ const plaintextFile = decryptFile(client, vaultClient, {
+ file,
+ decryptionKey
+ })
+ return new Blob([plaintextFile], { type: file.type })
}
export const downloadEncryptedFile = async (
client,
vaultClient,
- { file, encryptionKey }
+ { file, decryptionKey }
) => {
const url = await getDecryptedFileURL(client, vaultClient, {
file,
- encryptionKey
+ decryptionKey
})
return client.collection(DOCTYPE_FILES).forceFileDownload(url, file.name)
}
@@ -107,8 +182,13 @@ export const downloadEncryptedFile = async (
export const getDecryptedFileURL = async (
client,
vaultClient,
- { file, encryptionKey }
+ { file, decryptionKey }
) => {
- const blob = await decryptFile(client, vaultClient, { file, encryptionKey })
+ const plaintextFile = await decryptFile(client, vaultClient, {
+ file,
+ decryptionKey
+ })
+ const blob = new Blob([plaintextFile], { type: file.type })
+
return URL.createObjectURL(blob)
}
diff --git a/src/drive/mobile/modules/offline/duck.js b/src/drive/mobile/modules/offline/duck.js
index 4e80d65518..a489009786 100644
--- a/src/drive/mobile/modules/offline/duck.js
+++ b/src/drive/mobile/modules/offline/duck.js
@@ -11,7 +11,7 @@ import Alerter from 'cozy-ui/transpiled/react/Alerter'
import {
getEncryptionKeyFromDirId,
- decryptFile,
+ decryptFileToBlob,
isEncryptedFile
} from 'drive/lib/encryption'
import { openFileWith } from 'drive/web/modules/actions/utils'
@@ -73,8 +73,11 @@ export const saveOfflineFileCopy = async (file, client, { vaultClient }) => {
try {
let blob
if (isEncryptedFile(file)) {
- const encryptionKey = await getEncryptionKeyFromDirId(client, file.dir_id)
- blob = await decryptFile(client, vaultClient, { file, encryptionKey })
+ const decryptionKey = await getEncryptionKeyFromDirId(client, file.dir_id)
+ blob = await decryptFileToBlob(client, vaultClient, {
+ file,
+ decryptionKey
+ })
} else {
const response = await client
.collection(DOCTYPE_FILES)
diff --git a/src/drive/mobile/modules/upload/index.jsx b/src/drive/mobile/modules/upload/index.jsx
index 396eb1df2b..5beb094a36 100644
--- a/src/drive/mobile/modules/upload/index.jsx
+++ b/src/drive/mobile/modules/upload/index.jsx
@@ -135,9 +135,8 @@ export class DumbUpload extends Component {
>
diff --git a/src/drive/mobile/modules/upload/index.spec.js b/src/drive/mobile/modules/upload/index.spec.js
index 7133bb128b..5dea4bcbb6 100644
--- a/src/drive/mobile/modules/upload/index.spec.js
+++ b/src/drive/mobile/modules/upload/index.spec.js
@@ -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()
diff --git a/src/drive/web/modules/actions/index.spec.js b/src/drive/web/modules/actions/index.spec.js
index ce4e9363c5..86c45c3c92 100644
--- a/src/drive/web/modules/actions/index.spec.js
+++ b/src/drive/web/modules/actions/index.spec.js
@@ -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 = [
diff --git a/src/drive/web/modules/actions/utils.js b/src/drive/web/modules/actions/utils.js
index a7708313dc..38c40db0bf 100644
--- a/src/drive/web/modules/actions/utils.js
+++ b/src/drive/web/modules/actions/utils.js
@@ -10,7 +10,7 @@ import {
getEncryptionKeyFromDirId,
downloadEncryptedFile,
isEncryptedFolder,
- decryptFile,
+ decryptFileToBlob,
isEncryptedFile
} from 'drive/lib/encryption'
import { DOCTYPE_FILES } from 'drive/lib/doctypes'
@@ -48,16 +48,16 @@ const openFileDownloadError = error => {
* @param {array} files One or more files to download
*/
export const downloadFiles = async (client, files, { vaultClient } = {}) => {
- const encryptionKey = await getEncryptionKeyFromDirId(client, files[0].dir_id)
+ const decryptionKey = await getEncryptionKeyFromDirId(client, files[0].dir_id)
if (files.length === 1 && !isDirectory(files[0])) {
const file = files[0]
try {
const filename = file.name
- if (encryptionKey) {
+ if (decryptionKey) {
return downloadEncryptedFile(client, vaultClient, {
file,
- encryptionKey
+ decryptionKey
})
} else {
return client.collection(DOCTYPE_FILES).download(file, null, filename)
@@ -66,8 +66,8 @@ export const downloadFiles = async (client, files, { vaultClient } = {}) => {
Alerter.error(downloadFileError(error))
}
} else {
- if (encryptionKey) {
- // Multiple download is forbidden for encrypted files because we cannot generate client archive for now.
+ if (decryptionKey) {
+ // decryptionKey download is forbidden for encrypted files because we cannot generate client archive for now.
return Alerter.error('error.download_file.encryption_many')
}
const hasEncryptedDirs = files.find(
@@ -136,13 +136,16 @@ export const exportFilesNative = async (
files,
{ vaultClient } = {}
) => {
- const encryptionKey = isEncryptedFile(files[0])
+ const decryptionKey = isEncryptedFile(files[0])
? await getEncryptionKeyFromDirId(client, files[0].dir_id)
: null
const downloadAllFiles = files.map(async file => {
let blob
- if (encryptionKey) {
- blob = await decryptFile(client, vaultClient, { file, encryptionKey })
+ if (decryptionKey) {
+ blob = await decryptFileToBlob(client, vaultClient, {
+ file,
+ decryptionKey
+ })
} else {
const response = await client
.collection(DOCTYPE_FILES)
@@ -206,11 +209,14 @@ export const openFileWith = async (client, file, { vaultClient } = {}) => {
let originalMime = file.mime
try {
if (isEncryptedFile(file)) {
- const encryptionKey = await getEncryptionKeyFromDirId(
+ const decryptionKey = await getEncryptionKeyFromDirId(
client,
file.dir_id
)
- blob = await decryptFile(client, vaultClient, { file, encryptionKey })
+ blob = await decryptFileToBlob(client, vaultClient, {
+ file,
+ decryptionKey
+ })
originalMime = client
.collection(DOCTYPE_FILES)
.getFileTypeFromName(file.name)
diff --git a/src/drive/web/modules/actions/utils.spec.js b/src/drive/web/modules/actions/utils.spec.js
index cc561b42b3..fdfc756f05 100644
--- a/src/drive/web/modules/actions/utils.spec.js
+++ b/src/drive/web/modules/actions/utils.spec.js
@@ -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'
@@ -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', () => {
@@ -140,7 +140,7 @@ describe('downloadFiles', () => {
expect(downloadEncryptedFile).toHaveBeenCalledWith(
mockClient,
{},
- { file, encryptionKey: 'encryption-key' }
+ { file, decryptionKey: 'encryption-key' }
)
})
@@ -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', {
@@ -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'
}
)
)
diff --git a/src/drive/web/modules/filelist/AddFolder.spec.jsx b/src/drive/web/modules/filelist/AddFolder.spec.jsx
index 1f7c3ae674..617f64ce8d 100644
--- a/src/drive/web/modules/filelist/AddFolder.spec.jsx
+++ b/src/drive/web/modules/filelist/AddFolder.spec.jsx
@@ -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 = () => {
diff --git a/src/drive/web/modules/move/FileList.jsx b/src/drive/web/modules/move/FileList.jsx
index 6ba4281376..89c9abbe6a 100644
--- a/src/drive/web/modules/move/FileList.jsx
+++ b/src/drive/web/modules/move/FileList.jsx
@@ -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 (
-
{
- setShouldUnlock(false)
- return navigateTo(ROOT_DIR_ID)
- }}
- onUnlock={() => setShouldUnlock(false)}
- />
- )
- } else {
- return (
- <>
- {files.map(file => (
- 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 => (
+ 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
diff --git a/src/drive/web/modules/move/FileList.spec.jsx b/src/drive/web/modules/move/FileList.spec.jsx
new file mode 100644
index 0000000000..44274a678e
--- /dev/null
+++ b/src/drive/web/modules/move/FileList.spec.jsx
@@ -0,0 +1,119 @@
+import React from 'react'
+import { render } from '@testing-library/react'
+import { createMockClient } from 'cozy-client'
+import { DOCTYPE_FILES_ENCRYPTION } from 'drive/lib/doctypes'
+import AppLike from 'test/components/AppLike'
+import { generateFile } from 'test/generate'
+import FileList, { isInvalidMoveTarget } from 'drive/web/modules/move/FileList'
+
+jest.mock('cozy-keys-lib', () => ({
+ useVaultUnlockContext: jest
+ .fn()
+ .mockReturnValue({ showUnlockForm: jest.fn() })
+}))
+const client = createMockClient({})
+
+const setup = ({ files, entries, navigateTo }) => {
+ const root = render(
+
+
+
+ )
+
+ return { root }
+}
+
+describe('FileList', () => {
+ it('should display files', () => {
+ const entries = [generateFile({ i: '1', type: 'file' })]
+ const files = [
+ generateFile({ i: '1', type: 'directory', prefix: '' }),
+ generateFile({ i: '2', type: 'file', prefix: '' })
+ ]
+
+ const { root } = setup({ files, entries, navigateTo: () => null })
+ const { queryAllByText } = root
+
+ expect(queryAllByText('directory-1')).toBeTruthy()
+ expect(queryAllByText('file-2')).toBeTruthy()
+ })
+})
+
+describe('isInvalidMoveTarget', () => {
+ it('should return true when target is a file', () => {
+ const target = { _id: '1', type: 'file' }
+ const entries = [{ _id: 'dir1', type: 'directory' }]
+ expect(isInvalidMoveTarget(entries, target)).toBe(true)
+ })
+
+ it('should return true when subject is the target', () => {
+ const target = { _id: '1', type: 'directory' }
+ const entries = [{ _id: '1', type: 'directory' }]
+ expect(isInvalidMoveTarget(entries, target)).toBe(true)
+ })
+
+ it('should return true when target is an encrypted directory and entries has non-encrypted folder', () => {
+ const target = {
+ _id: '1',
+ type: 'directory',
+ referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }]
+ }
+ const entries = [{ _id: 'dir1', type: 'directory' }]
+ expect(isInvalidMoveTarget(entries, target)).toBe(true)
+ })
+
+ it('shold return true when target is an encrypted dir and entries include both encrytped and non-encrypted dir', () => {
+ const target = {
+ _id: '1',
+ type: 'directory',
+ referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }]
+ }
+ const entries = [
+ {
+ _id: 'dir1',
+ type: 'directory',
+ referenced_by: [
+ { type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }
+ ]
+ },
+ {
+ _id: 'dir2',
+ type: 'directory'
+ }
+ ]
+ expect(isInvalidMoveTarget(entries, target)).toBe(true)
+ })
+
+ it('should return false when both target and entries are encrypted', () => {
+ const target = {
+ _id: '1',
+ type: 'directory',
+ referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }]
+ }
+ const entries = [
+ {
+ _id: 'dir1',
+ type: 'directory',
+ referenced_by: [
+ { type: DOCTYPE_FILES_ENCRYPTION, id: 'encrypted-key-1' }
+ ]
+ }
+ ]
+ expect(isInvalidMoveTarget(entries, target)).toBe(false)
+ })
+
+ it('should return false when both target and entries are regular folders', () => {
+ const target = { _id: '1', type: 'directory' }
+ const entries = [{ _id: 'dir1', type: 'directory' }]
+ expect(isInvalidMoveTarget(entries, target)).toBe(false)
+ })
+
+ it('should return false when subject is a mix of regular file and folder and target a regular folder', () => {
+ const target = { _id: '1', type: 'directory' }
+ const entries = [
+ { _id: 'dir1', type: 'file' },
+ { _id: 'file1', type: 'file' }
+ ]
+ expect(isInvalidMoveTarget(entries, target)).toBe(false)
+ })
+})
diff --git a/src/drive/web/modules/move/MoveModal.jsx b/src/drive/web/modules/move/MoveModal.jsx
index 123bc96ff8..facbd7029b 100644
--- a/src/drive/web/modules/move/MoveModal.jsx
+++ b/src/drive/web/modules/move/MoveModal.jsx
@@ -6,6 +6,7 @@ import { withStyles } from '@material-ui/core/styles'
import { Query, cancelable, withClient, Q } from 'cozy-client'
import { CozyFile } from 'models'
+import { withVaultUnlockContext } from 'cozy-keys-lib'
import logger from 'lib/logger'
import { RefreshableSharings } from 'cozy-sharing'
@@ -16,7 +17,6 @@ import Alerter from 'cozy-ui/transpiled/react/Alerter'
import { getTracker } from 'cozy-ui/transpiled/react/helpers/tracker'
import { withBreakpoints } from 'cozy-ui/transpiled/react'
-import { ROOT_DIR_ID } from 'drive/constants/config'
import Header from 'drive/web/modules/move/Header'
import Explorer from 'drive/web/modules/move/Explorer'
import FileList from 'drive/web/modules/move/FileList'
@@ -30,6 +30,14 @@ import {
buildMoveOrImportQuery,
buildOnlyFolderQuery
} from 'drive/web/modules/queries'
+import {
+ isEncryptedFolder,
+ isEncryptedFile,
+ getEncryptionKeyFromDirId,
+ encryptAndUploadExistingFile,
+ decryptAndUploadExistingFile,
+ reencryptAndUploadExistingFile
+} from 'drive/lib/encryption'
const styles = theme => ({
paper: {
@@ -57,7 +65,7 @@ export class MoveModal extends React.Component {
this.promises = []
const { displayedFolder } = props
this.state = {
- folderId: displayedFolder ? displayedFolder._id : ROOT_DIR_ID,
+ targetFolder: displayedFolder,
isMoveInProgress: false
}
}
@@ -67,7 +75,7 @@ export class MoveModal extends React.Component {
}
navigateTo = folder => {
- this.setState({ folderId: folder._id })
+ this.setState({ targetFolder: folder })
}
registerCancelable = promise => {
@@ -82,21 +90,91 @@ export class MoveModal extends React.Component {
this.promises = []
}
+ moveEncryptedEntry = async (
+ client,
+ vaultClient,
+ entry,
+ { isEncryptedTarget, isEncryptedFileEntry, targetFolder, sourceFolder }
+ ) => {
+ if (isEncryptedTarget && !isEncryptedFileEntry) {
+ // The plaintext file is moved to an encrypted directory
+ const encryptionKey = await getEncryptionKeyFromDirId(
+ client,
+ targetFolder._id
+ )
+
+ await encryptAndUploadExistingFile(client, vaultClient, {
+ file: entry,
+ encryptionKey
+ })
+ } else if (!isEncryptedTarget && isEncryptedFileEntry) {
+ // The encrypted file is moved to a plaintext directory
+ const decryptionKey = await getEncryptionKeyFromDirId(
+ client,
+ sourceFolder._id
+ )
+
+ await decryptAndUploadExistingFile(client, vaultClient, {
+ file: entry,
+ decryptionKey
+ })
+ } else if (isEncryptedTarget && isEncryptedFileEntry) {
+ // The encrypted file is moved to another encrypted directory
+ const encryptionKey = await getEncryptionKeyFromDirId(
+ client,
+ targetFolder._id
+ )
+ const decryptionKey = await getEncryptionKeyFromDirId(
+ client,
+ sourceFolder._id
+ )
+ await reencryptAndUploadExistingFile(client, vaultClient, {
+ file: entry,
+ decryptionKey,
+ encryptionKey
+ })
+ }
+ }
+
+ moveEntry = async (
+ entry,
+ targetFolder,
+ { sharedPaths, isEncryptedTarget, client, vaultClient }
+ ) => {
+ const { displayedFolder } = this.props
+ const isEncryptedFileEntry = isEncryptedFile(entry)
+ if (isEncryptedTarget || isEncryptedFileEntry) {
+ await this.moveEncryptedEntry(client, vaultClient, entry, {
+ isEncryptedTarget,
+ isEncryptedFileEntry,
+ targetFolder,
+ sourceFolder: displayedFolder
+ })
+ }
+
+ const targetPath = await CozyFile.getFullpath(targetFolder._id, entry.name)
+ const force = !sharedPaths.includes(targetPath)
+ return CozyFile.move(entry._id, { folderId: targetFolder._id }, force)
+ }
+
moveEntries = async callback => {
- const { client, entries, onClose, sharingState, t } = this.props
+ const { client, vaultClient, entries, onClose, sharingState, t } =
+ this.props
const { sharedPaths } = sharingState
- const { folderId } = this.state
+ const { targetFolder } = this.state
try {
this.setState({ isMoveInProgress: true })
+ const isEncryptedTarget = isEncryptedFolder(targetFolder)
const trashedFiles = []
await Promise.all(
entries.map(async entry => {
- const targetPath = await this.registerCancelable(
- CozyFile.getFullpath(folderId, entry.name)
- )
- const force = !sharedPaths.includes(targetPath)
const moveResponse = await this.registerCancelable(
- CozyFile.move(entry._id, { folderId }, force)
+ this.moveEntry(entry, targetFolder, {
+ sharedPaths,
+ isEncryptedTarget,
+ client,
+ vaultClient
+ })
)
if (moveResponse.deleted) {
trashedFiles.push(moveResponse.deleted)
@@ -105,7 +183,7 @@ export class MoveModal extends React.Component {
)
const response = await this.registerCancelable(
- client.query(Q('io.cozy.files').getById(folderId))
+ client.query(Q('io.cozy.files').getById(targetFolder._id))
)
const targetName = response.data.name
Alerter.info('Move.success', {
@@ -113,7 +191,8 @@ export class MoveModal extends React.Component {
target: targetName,
smart_count: entries.length,
buttonText: t('Move.cancel'),
- buttonAction: () => this.cancelMove(entries, trashedFiles, callback)
+ buttonAction: () =>
+ this.cancelMove(entries, trashedFiles, callback, { targetFolder })
})
this.trackEvent(entries.length)
if (callback) callback()
@@ -129,14 +208,28 @@ export class MoveModal extends React.Component {
}
cancelMove = async (entries, trashedFiles, callback) => {
- const { client } = this.props
+ const { client, vaultClient, displayedFolder } = this.props
+ const { targetFolder } = this.state
try {
+ const isEncryptedDisplayedFolder = isEncryptedFolder(displayedFolder)
+ const isEncryptedTargetFolder = isEncryptedFolder(targetFolder)
+
await Promise.all(
- entries.map(entry =>
- this.registerCancelable(
+ entries.map(async entry => {
+ if (isEncryptedDisplayedFolder || isEncryptedTargetFolder) {
+ this.registerCancelable(
+ this.moveEncryptedEntry(client, vaultClient, entry, {
+ isEncryptedTarget: isEncryptedDisplayedFolder,
+ isEncryptedFileEntry: isEncryptedTargetFolder,
+ targetFolder: displayedFolder,
+ sourceFolder: targetFolder
+ })
+ )
+ }
+ return this.registerCancelable(
CozyFile.move(entry._id, { folderId: entry.dir_id })
)
- )
+ })
)
const fileCollection = client.collection(CozyFile.doctype)
let restoreErrorsCount = 0
@@ -183,10 +276,10 @@ export class MoveModal extends React.Component {
classes,
breakpoints: { isMobile }
} = this.props
- const { folderId, isMoveInProgress } = this.state
+ const { targetFolder, isMoveInProgress } = this.state
- const contentQuery = buildMoveOrImportQuery(folderId)
- const folderQuery = buildOnlyFolderQuery(folderId)
+ const contentQuery = buildMoveOrImportQuery(targetFolder._id)
+ const folderQuery = buildOnlyFolderQuery(targetFolder._id)
return (
{({ data, fetchStatus }) => (
{({ data, fetchStatus, hasMore, fetchMore }) => {
return (
@@ -231,7 +324,7 @@ export class MoveModal extends React.Component {
>
@@ -248,7 +341,7 @@ export class MoveModal extends React.Component {
onConfirm={() => this.moveEntries(refresh)}
onClose={onClose}
targets={entries}
- currentDirId={folderId}
+ currentDirId={targetFolder._id}
isMoving={isMoveInProgress}
/>
)}
@@ -276,6 +369,7 @@ export default compose(
connect(mapStateToProps),
translate(),
withClient,
+ withVaultUnlockContext,
withSharingState,
withStyles(styles),
withBreakpoints()
diff --git a/src/drive/web/modules/move/MoveModal.spec.jsx b/src/drive/web/modules/move/MoveModal.spec.jsx
index ba9ad4782f..36799bbec2 100644
--- a/src/drive/web/modules/move/MoveModal.spec.jsx
+++ b/src/drive/web/modules/move/MoveModal.spec.jsx
@@ -5,11 +5,29 @@ import CozyClient from 'cozy-client'
import { CozyFile } from 'models'
import { MoveModal } from './MoveModal'
+import { DOCTYPE_FILES_ENCRYPTION } from 'drive/lib/doctypes'
+import {
+ getEncryptionKeyFromDirId,
+ encryptAndUploadExistingFile,
+ decryptAndUploadExistingFile,
+ reencryptAndUploadExistingFile
+} from 'drive/lib/encryption'
jest.mock('cozy-client/dist/utils', () => ({
cancelable: jest.fn().mockImplementation(promise => promise)
}))
+jest.mock('cozy-keys-lib', () => ({
+ withVaultUnlockContext: jest.fn().mockReturnValue(<>>)
+}))
+jest.mock('drive/lib/encryption', () => ({
+ ...jest.requireActual('drive/lib/encryption'),
+ getEncryptionKeyFromDirId: jest.fn(),
+ encryptAndUploadExistingFile: jest.fn(),
+ decryptAndUploadExistingFile: jest.fn(),
+ reencryptAndUploadExistingFile: jest.fn()
+}))
+
jest.mock('cozy-doctypes')
jest.mock('cozy-stack-client')
@@ -44,10 +62,19 @@ describe('MoveModal component', () => {
sharedPaths: ['/sharedFolder', '/bills/bill_201903.pdf']
}
- const setupComponent = (entries = defaultEntries) => {
+ const defaultDisplayedFolder = { _id: 'bills' }
+ const encryptedDisplayedFolder = {
+ _id: 'encrypted',
+ referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: '123' }]
+ }
+
+ const setupComponent = (
+ entries = defaultEntries,
+ displayedFolder = defaultDisplayedFolder
+ ) => {
const props = {
client: cozyClient,
- displayedFolder: { _id: 'bills' },
+ displayedFolder,
entries,
onClose: onCloseSpy,
sharingState,
@@ -60,8 +87,8 @@ describe('MoveModal component', () => {
describe('moveEntries', () => {
it('should move entries to destination', async () => {
- const component = setupComponent(defaultEntries, sharingState)
- component.setState({ folderId: 'destinationFolder' })
+ const component = setupComponent(defaultEntries)
+ component.setState({ targetFolder: { _id: 'destinationFolder' } })
CozyFile.getFullpath.mockImplementation((destinationFolder, name) =>
Promise.resolve(
name === 'bill_201903.pdf' ? '/bills/bill_201903.pdf' : '/whatever'
@@ -112,6 +139,81 @@ describe('MoveModal component', () => {
expect(cb).toHaveBeenCalled()
// TODO: check that trashedFiles are passed to cancel button
})
+
+ it('should move non-encrypted files to encrypted dir', async () => {
+ const component = setupComponent(defaultEntries)
+ component.setState({
+ targetFolder: {
+ _id: 'destinationFolder',
+ referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: '123' }]
+ }
+ })
+ CozyFile.move.mockImplementation(id => ({ moved: { id } }))
+ const cb = jest.fn()
+ await component.instance().moveEntries(cb)
+
+ expect(getEncryptionKeyFromDirId).toHaveBeenCalled()
+ expect(encryptAndUploadExistingFile).toHaveBeenCalled()
+ })
+
+ it('should move encrypted files to non-encrypted dir', async () => {
+ const entries = [
+ {
+ _id: 'bill_201901',
+ dir_id: 'bills',
+ name: 'bill_201901.pdf',
+ encrypted: true
+ },
+ {
+ _id: 'bill_201901',
+ dir_id: 'bills',
+ name: 'bill_201901.pdf',
+ encrypted: true
+ }
+ ]
+ const component = setupComponent(entries)
+ component.setState({
+ targetFolder: {
+ _id: 'destinationFolder'
+ }
+ })
+ CozyFile.move.mockImplementation(id => ({ moved: { id } }))
+ const cb = jest.fn()
+ await component.instance().moveEntries(cb)
+
+ expect(getEncryptionKeyFromDirId).toHaveBeenCalled()
+ expect(decryptAndUploadExistingFile).toHaveBeenCalled()
+ })
+
+ it('should move encrypted files to encrypted dir', async () => {
+ const entries = [
+ {
+ _id: 'bill_201901',
+ dir_id: 'bills',
+ name: 'bill_201901.pdf',
+ encrypted: true
+ },
+ {
+ _id: 'bill_201901',
+ dir_id: 'bills',
+ name: 'bill_201901.pdf',
+ encrypted: true
+ }
+ ]
+ const component = setupComponent(entries)
+ component.setState({
+ targetFolder: {
+ _id: 'destinationFolder',
+ referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: '123' }]
+ }
+ })
+ CozyFile.move.mockImplementation(id => ({ moved: { id } }))
+ const cb = jest.fn()
+ await component.instance().moveEntries(cb)
+
+ expect(getEncryptionKeyFromDirId).toHaveBeenCalled()
+ expect(reencryptAndUploadExistingFile).toHaveBeenCalled()
+ })
})
describe('cancelMove', () => {
@@ -140,5 +242,73 @@ describe('MoveModal component', () => {
expect(restoreSpy).toHaveBeenCalledWith('trashed-2')
expect(callback).toHaveBeenCalled()
})
+
+ it('should move back files moved from non-encrypted dir to encrypted dir', async () => {
+ const component = setupComponent(defaultEntries)
+ component.setState({
+ targetFolder: {
+ _id: 'destinationFolder',
+ referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: '123' }]
+ }
+ })
+ const callback = jest.fn()
+ await component.instance().cancelMove(defaultEntries, [], callback)
+ expect(getEncryptionKeyFromDirId).toHaveBeenCalled()
+ expect(decryptAndUploadExistingFile).toHaveBeenCalled()
+
+ expect(CozyFile.move).toHaveBeenCalledWith('bill_201901', {
+ folderId: 'bills'
+ })
+ expect(CozyFile.move).toHaveBeenCalledWith('bill_201902', {
+ folderId: 'bills'
+ })
+ expect(restoreSpy).not.toHaveBeenCalled()
+ expect(callback).toHaveBeenCalled()
+ })
+
+ it('should move back files moved from encrypted dir to non-encrypted dir', async () => {
+ const component = setupComponent(defaultEntries, encryptedDisplayedFolder)
+ component.setState({
+ targetFolder: {
+ _id: 'destinationFolder'
+ }
+ })
+ const callback = jest.fn()
+ await component.instance().cancelMove(defaultEntries, [], callback)
+ expect(getEncryptionKeyFromDirId).toHaveBeenCalled()
+ expect(encryptAndUploadExistingFile).toHaveBeenCalled()
+
+ expect(CozyFile.move).toHaveBeenCalledWith('bill_201901', {
+ folderId: 'bills'
+ })
+ expect(CozyFile.move).toHaveBeenCalledWith('bill_201902', {
+ folderId: 'bills'
+ })
+ expect(restoreSpy).not.toHaveBeenCalled()
+ expect(callback).toHaveBeenCalled()
+ })
+
+ it('should move back files moved from encrypted dir to another encrypted dir', async () => {
+ const component = setupComponent(defaultEntries, encryptedDisplayedFolder)
+ component.setState({
+ targetFolder: {
+ _id: 'destinationFolder',
+ referenced_by: [{ type: DOCTYPE_FILES_ENCRYPTION, id: '123' }]
+ }
+ })
+ const callback = jest.fn()
+ await component.instance().cancelMove(defaultEntries, [], callback)
+ expect(getEncryptionKeyFromDirId).toHaveBeenCalled()
+ expect(reencryptAndUploadExistingFile).toHaveBeenCalled()
+
+ expect(CozyFile.move).toHaveBeenCalledWith('bill_201901', {
+ folderId: 'bills'
+ })
+ expect(CozyFile.move).toHaveBeenCalledWith('bill_201902', {
+ folderId: 'bills'
+ })
+ expect(restoreSpy).not.toHaveBeenCalled()
+ expect(callback).toHaveBeenCalled()
+ })
})
})
diff --git a/src/drive/web/modules/navigation/duck/actions.spec.jsx b/src/drive/web/modules/navigation/duck/actions.spec.jsx
index 88e74c97d0..c89f047589 100644
--- a/src/drive/web/modules/navigation/duck/actions.spec.jsx
+++ b/src/drive/web/modules/navigation/duck/actions.spec.jsx
@@ -9,7 +9,8 @@ import { createFolder } from './actions'
jest.mock('cozy-keys-lib', () => ({
withVaultClient: jest.fn().mockReturnValue({}),
useVaultClient: jest.fn(),
- WebVaultClient: jest.fn().mockReturnValue({})
+ WebVaultClient: jest.fn().mockReturnValue({}),
+ withVaultUnlockContext: jest.fn().mockReturnValue({})
}))
const vaultClient = new WebVaultClient('http://alice.cozy.cloud')
diff --git a/src/drive/web/modules/public/LightFileViewer.spec.jsx b/src/drive/web/modules/public/LightFileViewer.spec.jsx
index d45c2b4d4c..5a2c2050a2 100644
--- a/src/drive/web/modules/public/LightFileViewer.spec.jsx
+++ b/src/drive/web/modules/public/LightFileViewer.spec.jsx
@@ -16,6 +16,10 @@ jest.mock('cozy-ui/transpiled/react/hooks/useBreakpoints', () => ({
default: jest.fn(),
BreakpointsProvider: ({ children }) => children
}))
+jest.mock('cozy-keys-lib', () => ({
+ withVaultUnlockContext: jest.fn().mockReturnValue(<>>),
+ withVaultClient: jest.fn().mockReturnValue(<>>)
+}))
const client = new createMockClient({})
diff --git a/src/drive/web/modules/upload/index.js b/src/drive/web/modules/upload/index.js
index a211868868..1339b7e1c4 100644
--- a/src/drive/web/modules/upload/index.js
+++ b/src/drive/web/modules/upload/index.js
@@ -337,7 +337,7 @@ const uploadFile = async (client, file, dirID, options = {}) => {
const fr = new FileReader()
fr.onloadend = async () => {
return encryptAndUploadNewFile(client, vaultClient, {
- file: fr.result,
+ binary: fr.result,
encryptionKey,
fileOptions: {
name: file.name,
diff --git a/src/drive/web/modules/viewer/FilesViewer.jsx b/src/drive/web/modules/viewer/FilesViewer.jsx
index 31a14c6861..f9ef704ee2 100644
--- a/src/drive/web/modules/viewer/FilesViewer.jsx
+++ b/src/drive/web/modules/viewer/FilesViewer.jsx
@@ -131,13 +131,13 @@ const FilesViewer = ({ filesQuery, files, fileId, onClose, onChange }) => {
const getDecryptedURLIfNecessary = async () => {
const file = files[currentIndex]
if (file && isEncryptedFile(file)) {
- const encryptionKey = await getEncryptionKeyFromDirId(
+ const decryptionKey = await getEncryptionKeyFromDirId(
client,
file.dir_id
)
const url = await getDecryptedFileURL(client, vaultClient, {
file,
- encryptionKey
+ decryptionKey
})
setCurrentDecryptedFileURL(url)
}
diff --git a/src/drive/web/modules/viewer/FilesViewer.spec.jsx b/src/drive/web/modules/viewer/FilesViewer.spec.jsx
index 375d8a2b69..f927d31c94 100644
--- a/src/drive/web/modules/viewer/FilesViewer.spec.jsx
+++ b/src/drive/web/modules/viewer/FilesViewer.spec.jsx
@@ -10,7 +10,8 @@ import { getEncryptionKeyFromDirId } from 'drive/lib/encryption'
jest.mock('cozy-client/dist/hooks/useQuery', () => jest.fn())
jest.mock('cozy-keys-lib', () => ({
- useVaultClient: jest.fn()
+ useVaultClient: jest.fn(),
+ withVaultUnlockContext: jest.fn().mockReturnValue({})
}))
jest.mock('drive/lib/encryption', () => ({
diff --git a/src/drive/web/modules/viewer/helpers.js b/src/drive/web/modules/viewer/helpers.js
index eeeb02fb76..480ca1af95 100644
--- a/src/drive/web/modules/viewer/helpers.js
+++ b/src/drive/web/modules/viewer/helpers.js
@@ -6,8 +6,8 @@ import {
export const downloadFile = async (client, file, { vaultClient }) => {
if (isEncryptedFile(file)) {
- const encryptionKey = await getEncryptionKeyFromDirId(client, file.dir_id)
- return downloadEncryptedFile(client, vaultClient, { file, encryptionKey })
+ const decryptionKey = await getEncryptionKeyFromDirId(client, file.dir_id)
+ return downloadEncryptedFile(client, vaultClient, { file, decryptionKey })
} else {
return client.collection('io.cozy.files').download(file)
}
diff --git a/test/setup.jsx b/test/setup.jsx
index de9a256e2c..534a392390 100644
--- a/test/setup.jsx
+++ b/test/setup.jsx
@@ -13,25 +13,6 @@ import FolderContent from 'test/components/FolderContent'
import { generateFile } from './generate'
import { act } from 'react-dom/test-utils'
-jest.mock('cozy-keys-lib', () => ({
- withVaultClient: BaseComponent => {
- const Component = props => (
- <>
- {({ vaultClient }) => (
-
- )}
- >
- )
-
- Component.displayName = `withVaultClient(${
- BaseComponent.displayName || BaseComponent.name
- })`
-
- return Component
- },
- useVaultClient: jest.fn()
-}))
-
configure({ testIdAttribute: 'data-testid' })
export const mockCozyClientRequestQuery = () => {