Repository: cvat-ai/cvat
Pull request: Share without copying & mount cloud storages
<!---
Copyright (C) 2020 Intel Corporation

SPDX-License-Identifier: MIT
-->

<!-- Raised an issue to propose your change (https://github.com/opencv/cvat/issues).
It will help avoiding duplication of efforts from multiple independent contributors.
Discuss your ideas with maintainers to be sure that changes will be approved and merged.
Read the [CONTRIBUTION](https://github.com/opencv/cvat/blob/develop/CONTRIBUTING.md)
guide. -->

<!-- Provide a general summary of your changes in the Title above -->

### Motivation and context
Related issue: #204

This PR will allow not copy files inside CVAT when creating a task using share files.
Also PR contains documentation about mounting cloud storage(e.g AWS S3, Azure container, Google Drive) as FUSE. 

### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->
Manually, tests
### Checklist
- [x] fix uploaded archive/zip
- [x] check automount using fstab
- [x] check automount using systemd


### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply.
If an item isn't applicable by a reason then ~~explicitly strikethrough~~ the whole
line. If you don't do that github will show incorrect process for the pull request.
If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] I submit my changes into the `develop` branch
- [ ] I have added description of my changes into [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file
- [ ] I have updated the [documentation](
  https://github.com/opencv/cvat/blob/develop/README.md#documentation) accordingly
- [x] I have added tests to cover my changes
- [ ] I have linked related issues ([read github docs](
  https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
- [ ] I have increased versions of npm packages if it is necessary ([cvat-canvas](https://github.com/opencv/cvat/tree/develop/cvat-canvas#versioning),
[cvat-core](https://github.com/opencv/cvat/tree/develop/cvat-core#versioning), [cvat-data](https://github.com/opencv/cvat/tree/develop/cvat-data#versioning) and [cvat-ui](https://github.com/opencv/cvat/tree/develop/cvat-ui#versioning))

### License

- [x] I submit _my code changes_ under the same [MIT License](
  https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the project.
  Feel free to contact the maintainers if that's a concern.
- [ ] I have updated the license header for each file (see an example below)

```python
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
```


Problem statement:
Any way to avoid copying and compressing files when creating new task?
Hi,

Is there any way to avoid copying and compressing images when a new task is created with Source: Shared? Also, source is bound read-only so it shouldn't overwrite anything.
Not sure what the reason is but I have some high resolution imagery and I am loosing the important details to set proper attributes and polygon masks. Also, it takes up unnecessary time and disk space. Thanks.

I am using cvat github version from 2018-11-19.

Hints from issue discussion:
Same here, I have a lot of HD images on net share and s3,
I'd like to map them to /share dir and use only links, without any data copy.
any updates on this ?
@vfdev-5 , we are going to reimplement our way to serve data from server (https://github.com/opencv/cvat/tree/az/video_stream). I hope to see the functionality merged in a month or so. After it is merged we will think how to implement the feature. Probably it will be possible to provide data in a pre-defined format. I hope to see the feature in v1.0.0. But we cannot promise.
Some notes for future reference.

Pipeline to use original data:
1. Prepare data in a format which CVAT can understand and put them onto your remote storage:
     * directory with images
     * Use CVAT script (will be provided) to prepare data in right format (chunks)
     * Use CVAT script (will be provided) to prepare data for "protected" access (e.g. S3 with credentials). Thus instead of original data the user will upload some text files with links on original data and meta information about data.
2. CVAT will use the remote storage as is and don't try to copy files internally. There are three main use cases:
    * For `directory with images` CVAT will convert them into "own format" on the fly and cache using DiskCache (http://www.grantjenks.com/docs/diskcache/) in a temporary directory (thus next access should be fast and storage size will be limited).
    * Serve original data as is if data was prepared using CVAT script. Aka remote links on the required data. The user already prepared original data for us, don't need to do anything else. But for compressed data we need to compress them on the fly and cache using DiskCache (http://www.grantjenks.com/docs/diskcache/) in a temporary directory (thus next access should be fast and storage size will be limited). Because the original data was prepared as small chunks it will be fast enough to prepare compressed data.
3. cvat-data will be responsible to accept prepared data and transform them to actual images with meta information.
    * chunkN.zip with images will be unzipped
    * chunkN.mp4 with video frames will be decoded
    * chunkN.txt with links to data will be converted to images if credentials are provided by the client.

Commit message: Fixed typo

Patch to review:
diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json
index 6128b1d4aa18..dd36746135c6 100644
--- a/cvat-core/package-lock.json
+++ b/cvat-core/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "cvat-core",
-  "version": "3.8.1",
+  "version": "3.9.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/cvat-core/package.json b/cvat-core/package.json
index 0227549ffdfd..84140ddef9a3 100644
--- a/cvat-core/package.json
+++ b/cvat-core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "cvat-core",
-  "version": "3.8.1",
+  "version": "3.9.0",
   "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
   "main": "babel.config.js",
   "scripts": {
diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js
index 8c47fa43a898..35a3125afbc8 100644
--- a/cvat-core/src/session.js
+++ b/cvat-core/src/session.js
@@ -875,6 +875,7 @@
                 data_original_chunk_type: undefined,
                 use_zip_chunks: undefined,
                 use_cache: undefined,
+                copy_data: undefined,
             };
 
             for (const property in data) {
@@ -1117,6 +1118,22 @@
                             data.use_cache = useCache;
                         },
                     },
+                    /**
+                     * @name copyData
+                     * @type {boolean}
+                     * @memberof module:API.cvat.classes.Task
+                     * @instance
+                     * @throws {module:API.cvat.exceptions.ArgumentError}
+                     */
+                    copyData: {
+                        get: () => data.copy_data,
+                        set: (copyData) => {
+                            if (typeof copyData !== 'boolean') {
+                                throw new ArgumentError('Value must be a boolean');
+                            }
+                            data.copy_data = copyData;
+                        },
+                    },
                     /**
                      * After task has been created value can be appended only.
                      * @name labels
@@ -1695,6 +1712,9 @@
         if (typeof this.dataChunkSize !== 'undefined') {
             taskDataSpec.chunk_size = this.dataChunkSize;
         }
+        if (typeof this.copyData !== 'undefined') {
+            taskDataSpec.copy_data = this.copyData;
+        }
 
         const task = await serverProxy.tasks.createTask(taskSpec, taskDataSpec, onUpdate);
         return new Task(task);
diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json
index 2c4f592ba013..2a90cc406ef9 100644
--- a/cvat-ui/package-lock.json
+++ b/cvat-ui/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "cvat-ui",
-  "version": "1.9.14",
+  "version": "1.10.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/cvat-ui/package.json b/cvat-ui/package.json
index 4a5aa1b7e01e..c291129f396f 100644
--- a/cvat-ui/package.json
+++ b/cvat-ui/package.json
@@ -1,6 +1,6 @@
 {
   "name": "cvat-ui",
-  "version": "1.9.14",
+  "version": "1.10.0",
   "description": "CVAT single-page application",
   "main": "src/index.tsx",
   "scripts": {
diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts
index 1f6eebdb95e4..58b492f202e3 100644
--- a/cvat-ui/src/actions/tasks-actions.ts
+++ b/cvat-ui/src/actions/tasks-actions.ts
@@ -405,6 +405,9 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
         if (data.advanced.dataChunkSize) {
             description.data_chunk_size = data.advanced.dataChunkSize;
         }
+        if (data.advanced.copyData) {
+            description.copy_data = data.advanced.copyData;
+        }
 
         const taskInstance = new cvat.classes.Task(description);
         taskInstance.clientFiles = data.files.local;
diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx
index 88175b7e6aca..088831007348 100644
--- a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx
+++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx
@@ -26,11 +26,14 @@ export interface AdvancedConfiguration {
     useZipChunks: boolean;
     dataChunkSize?: number;
     useCache: boolean;
+    activeTab: string;
+    copyData?: boolean;
 }
 
 type Props = FormComponentProps & {
     onSubmit(values: AdvancedConfiguration): void;
     installedGit: boolean;
+    activeTab: string;
 };
 
 function isPositiveInteger(_: any, value: any, callback: any): void {
@@ -78,7 +81,7 @@ function isIntegerRange(min: number, max: number, _: any, value: any, callback:
 class AdvancedConfigurationForm extends React.PureComponent<Props> {
     public submit(): Promise<void> {
         return new Promise((resolve, reject) => {
-            const { form, onSubmit } = this.props;
+            const { form, onSubmit, activeTab } = this.props;
 
             form.validateFields((error, values): void => {
                 if (!error) {
@@ -100,6 +103,7 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
                     onSubmit({
                         ...values,
                         frameFilter: values.frameStep ? `step=${values.frameStep}` : undefined,
+                        activeTab: activeTab,
                     });
                     resolve();
                 } else {
@@ -114,6 +118,26 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
         form.resetFields();
     }
 
+    renderCopyDataChechbox(): JSX.Element {
+        const { form } = this.props;
+        return (
+            <Row>
+                <Col>
+                    <Form.Item help='If you have a low data transfer rate over the network you can copy data into CVAT to speed up work'>
+                        {form.getFieldDecorator('copyData', {
+                            initialValue: false,
+                            valuePropName: 'checked',
+                        })(
+                            <Checkbox>
+                                <Text className='cvat-text-color'>Copy data into CVAT</Text>
+                            </Checkbox>,
+                        )}
+                    </Form.Item>
+                </Col>
+            </Row>
+        );
+    }
+
     private renderImageQuality(): JSX.Element {
         const { form } = this.props;
 
@@ -386,10 +410,12 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
     }
 
     public render(): JSX.Element {
-        const { installedGit } = this.props;
-
+        const { installedGit, activeTab } = this.props;
         return (
             <Form>
+
+                {activeTab === 'share' ? this.renderCopyDataChechbox() : null}
+
                 <Row>
                     <Col>{this.renderUzeZipChunks()}</Col>
                 </Row>
diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx
index f30e79810104..c8964256f07b 100644
--- a/cvat-ui/src/components/create-task-page/create-task-content.tsx
+++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx
@@ -42,6 +42,7 @@ const defaultState = {
         lfs: false,
         useZipChunks: true,
         useCache: true,
+        activeTab: 'local',
     },
     labels: [],
     files: {
@@ -114,6 +115,16 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
         });
     };
 
+    private changeFileManagerTab = (key: string): void => {
+        const values = this.state.advanced;
+        this.setState({
+            advanced: {
+                ...values,
+                activeTab: key
+            }
+        });
+    };
+
     private handleSubmitClick = (): void => {
         if (!this.validateLabels()) {
             notification.error({
@@ -192,6 +203,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
                 <Text type='danger'>* </Text>
                 <Text className='cvat-text-color'>Select files:</Text>
                 <ConnectedFileManager
+                    onChangeActiveKey={this.changeFileManagerTab}
                     ref={(container: any): void => {
                         this.fileManagerContainer = container;
                     }}
@@ -209,6 +221,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
                     <Collapse.Panel key='1' header={<Text className='cvat-title'>Advanced configuration</Text>}>
                         <AdvancedConfigurationForm
                             installedGit={installedGit}
+                            activeTab={this.state.advanced.activeTab}
                             wrappedComponentRef={(component: any): void => {
                                 this.advancedConfigurationComponent = component;
                             }}
diff --git a/cvat-ui/src/components/file-manager/file-manager.tsx b/cvat-ui/src/components/file-manager/file-manager.tsx
index d62d724b412a..c4b9e2a875ad 100644
--- a/cvat-ui/src/components/file-manager/file-manager.tsx
+++ b/cvat-ui/src/components/file-manager/file-manager.tsx
@@ -31,6 +31,7 @@ interface Props {
     withRemote: boolean;
     treeData: TreeNodeNormal[];
     onLoadData: (key: string, success: () => void, failure: () => void) => void;
+    onChangeActiveKey(key: string): void;
 }
 
 export default class FileManager extends React.PureComponent<Props, State> {
@@ -215,7 +216,7 @@ export default class FileManager extends React.PureComponent<Props, State> {
     }
 
     public render(): JSX.Element {
-        const { withRemote } = this.props;
+        const { withRemote, onChangeActiveKey } = this.props;
         const { active } = this.state;
 
         return (
@@ -224,11 +225,12 @@ export default class FileManager extends React.PureComponent<Props, State> {
                     type='card'
                     activeKey={active}
                     tabBarGutter={5}
-                    onChange={(activeKey: string): void =>
+                    onChange={(activeKey: string): void => {
+                        onChangeActiveKey(activeKey);
                         this.setState({
                             active: activeKey as any,
-                        })
-                    }
+                        });
+                    }}
                 >
                     {this.renderLocalSelector()}
                     {this.renderShareSelector()}
diff --git a/cvat-ui/src/containers/file-manager/file-manager.tsx b/cvat-ui/src/containers/file-manager/file-manager.tsx
index 8a74964c1205..3db64770e166 100644
--- a/cvat-ui/src/containers/file-manager/file-manager.tsx
+++ b/cvat-ui/src/containers/file-manager/file-manager.tsx
@@ -14,6 +14,7 @@ import { ShareItem, CombinedState } from 'reducers/interfaces';
 interface OwnProps {
     ref: any;
     withRemote: boolean;
+    onChangeActiveKey(key: string): void;
 }
 
 interface StateToProps {
@@ -68,12 +69,13 @@ export class FileManagerContainer extends React.PureComponent<Props> {
     }
 
     public render(): JSX.Element {
-        const { treeData, getTreeData, withRemote } = this.props;
+        const { treeData, getTreeData, withRemote, onChangeActiveKey } = this.props;
 
         return (
             <FileManagerComponent
                 treeData={treeData}
                 onLoadData={getTreeData}
+                onChangeActiveKey={onChangeActiveKey}
                 withRemote={withRemote}
                 ref={(component): void => {
                     this.managerComponentRef = component;
diff --git a/cvat/apps/documentation/installation.md b/cvat/apps/documentation/installation.md
index ca536ea5580e..8abd316daa1e 100644
--- a/cvat/apps/documentation/installation.md
+++ b/cvat/apps/documentation/installation.md
@@ -373,6 +373,9 @@ You can change the share device path to your actual share. For user convenience
 we have defined the environment variable \$CVAT_SHARE_URL. This variable
 contains a text (url for example) which is shown in the client-share browser.
 
+You can [mount](/cvat/apps/documentation/mounting_cloud_storages.md)
+your cloud storage as a FUSE and use it later as a share.
+
 ### Email verification
 
 You can enable email verification for newly registered users.
diff --git a/cvat/apps/documentation/mounting_cloud_storages.md b/cvat/apps/documentation/mounting_cloud_storages.md
new file mode 100644
index 000000000000..f8d1de552574
--- /dev/null
+++ b/cvat/apps/documentation/mounting_cloud_storages.md
@@ -0,0 +1,385 @@
+- [Mounting cloud storage](#mounting-cloud-storage)
+  - [AWS S3 bucket](#aws-s3-bucket-as-filesystem)
+    - [Ubuntu 20.04](#aws_s3_ubuntu_2004)
+      - [Mount](#aws_s3_mount)
+      - [Automatically mount](#aws_s3_automatically_mount)
+        - [Using /etc/fstab](#aws_s3_using_fstab)
+        - [Using systemd](#aws_s3_using_systemd)
+      - [Check](#aws_s3_check)
+      - [Unmount](#aws_s3_unmount_filesystem)
+  - [Azure container](#microsoft-azure-container-as-filesystem)
+    - [Ubuntu 20.04](#azure_ubuntu_2004)
+      - [Mount](#azure_mount)
+      - [Automatically mount](#azure_automatically_mount)
+        - [Using /etc/fstab](#azure_using_fstab)
+        - [Using systemd](#azure_using_systemd)
+      - [Check](#azure_check)
+      - [Unmount](#azure_unmount_filesystem)
+  - [Google Drive](#google-drive-as-filesystem)
+    - [Ubuntu 20.04](#google_drive_ubuntu_2004)
+      - [Mount](#google_drive_mount)
+      - [Automatically mount](#google_drive_automatically_mount)
+        - [Using /etc/fstab](#google_drive_using_fstab)
+        - [Using systemd](#google_drive_using_systemd)
+      - [Check](#google_drive_check)
+      - [Unmount](#google_drive_unmount_filesystem)
+
+# Mounting cloud storage
+## AWS S3 bucket as filesystem
+### <a name="aws_s3_ubuntu_2004">Ubuntu 20.04</a>
+#### <a name="aws_s3_mount">Mount</a>
+
+1.  Install s3fs:
+
+    ```bash
+    sudo apt install s3fs
+    ```
+
+1.  Enter your credentials in a file  `${HOME}/.passwd-s3fs`  and set owner-only permissions:
+
+    ```bash
+    echo ACCESS_KEY_ID:SECRET_ACCESS_KEY > ${HOME}/.passwd-s3fs
+    chmod 600 ${HOME}/.passwd-s3fs
+    ```
+
+1.  Uncomment `user_allow_other` in the `/etc/fuse.conf` file: `sudo nano /etc/fuse.conf`
+1.  Run s3fs, replace `bucket_name`, `mount_point`:
+
+    ```bash
+    s3fs <bucket_name> <mount_point> -o allow_other
+    ```
+
+For more details see [here](https://github.com/s3fs-fuse/s3fs-fuse).
+
+#### <a name="aws_s3_automatically_mount">Automatically mount</a>
+Follow the first 3 mounting steps above.
+
+##### <a name="aws_s3_using_fstab">Using fstab</a>
+
+1.  Create a bash script named aws_s3_fuse(e.g in /usr/bin, as root) with this content
+    (replace `user_name` on whose behalf the disk will be mounted, `backet_name`, `mount_point`, `/path/to/.passwd-s3fs`):
+
+    ```bash
+    #!/bin/bash
+    sudo -u <user_name> s3fs <backet_name> <mount_point> -o passwd_file=/path/to/.passwd-s3fs -o allow_other
+    exit 0
+    ```
+
+1.  Give it the execution permission:
+
+    ```bash
+    sudo chmod +x /usr/bin/aws_s3_fuse
+    ```
+
+1.  Edit `/etc/fstab` adding a line like this, replace `mount_point`):
+
+    ```bash
+    /absolute/path/to/aws_s3_fuse  <mount_point>     fuse    allow_other,user,_netdev     0       0
+    ```
+
+##### <a name="aws_s3_using_systemd">Using systemd</a>
+
+1.  Create unit file `sudo nano /etc/systemd/system/s3fs.service`
+    (replace `user_name`, `bucket_name`, `mount_point`, `/path/to/.passwd-s3fs`):
+
+    ```bash
+    [Unit]
+    Description=FUSE filesystem over AWS S3 bucket
+    After=network.target
+
+    [Service]
+    Environment="MOUNT_POINT=<mount_point>"
+    User=<user_name>
+    Group=<user_name>
+    ExecStart=s3fs <bucket_name> ${MOUNT_POINT} -o passwd_file=/path/to/.passwd-s3fs -o allow_other
+    ExecStop=fusermount -u ${MOUNT_POINT}
+    Restart=always
+    Type=forking
+
+    [Install]
+    WantedBy=multi-user.target
+    ```
+
+1.  Update the system configurations, enable unit autorun when the system boots, mount the bucket:
+
+    ```bash
+    sudo systemctl daemon-reload
+    sudo systemctl enable s3fs.service
+    sudo systemctl start s3fs.service
+    ```
+
+#### <a name="aws_s3_check">Check</a>
+A file `/etc/mtab` contains records of currently mounted filesystems.
+```bash
+cat /etc/mtab | grep 's3fs'
+```
+
+#### <a name="aws_s3_unmount_filesystem">Unmount filesystem</a>
+```bash
+fusermount -u <mount_point>
+```
+
+If you used [systemd](#aws_s3_using_systemd) to mount a bucket:
+
+```bash
+sudo systemctl stop s3fs.service
+sudo systemctl disable s3fs.service
+```
+
+## Microsoft Azure container as filesystem
+### <a name="azure_ubuntu_2004">Ubuntu 20.04</a>
+#### <a name="azure_mount">Mount</a>
+1.  Set up the Microsoft package repository.(More [here](https://docs.microsoft.com/en-us/windows-server/administration/Linux-Package-Repository-for-Microsoft-Software#configuring-the-repositories))
+
+    ```bash
+    wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb
+    sudo dpkg -i packages-microsoft-prod.deb
+    sudo apt-get update
+    ```
+
+1.  Install `blobfuse` and `fuse`:
+
+    ```bash
+    sudo apt-get install blobfuse fuse
+    ```
+    For more details see [here](https://github.com/Azure/azure-storage-fuse/wiki/1.-Installation)
+
+1.  Create enviroments(replace `account_name`, `account_key`, `mount_point`):
+
+    ```bash
+    export AZURE_STORAGE_ACCOUNT=<account_name>
+    export AZURE_STORAGE_ACCESS_KEY=<account_key>
+    MOUNT_POINT=<mount_point>
+    ```
+
+1.  Create a folder for cache:
+    ```bash
+    sudo mkdir -p /mnt/blobfusetmp
+    ```
+
+1.  Make sure the file must be owned by the user who mounts the container:
+    ```bash
+    sudo chown <user> /mnt/blobfusetmp
+    ```
+
+1.  Create the mount point, if it doesn't exists:
+    ```bash
+    mkdir -p ${MOUNT_POINT}
+    ```
+
+1.  Uncomment `user_allow_other` in the `/etc/fuse.conf` file: `sudo nano /etc/fuse.conf`
+1.  Mount container(replace `your_container`):
+
+    ```bash
+    blobfuse ${MOUNT_POINT} --container-name=<your_container> --tmp-path=/mnt/blobfusetmp -o allow_other
+    ```
+
+#### <a name="azure_automatically_mount">Automatically mount</a>
+Follow the first 7 mounting steps above.
+##### <a name="azure_using_fstab">Using fstab</a>
+
+1.  Create configuration file `connection.cfg` with same content, change accountName,
+    select one from accountKey or sasToken and replace with your value:
+
+    ```bash
+    accountName <account-name-here>
+    # Please provide either an account key or a SAS token, and delete the other line.
+    accountKey <account-key-here-delete-next-line>
+    #change authType to specify only 1
+    sasToken <shared-access-token-here-delete-previous-line>
+    authType <MSI/SAS/SPN/Key/empty>
+    containerName <insert-container-name-here>
+    ```
+
+1.  Create a bash script named `azure_fuse`(e.g in /usr/bin, as root) with content below
+    (replace `user_name` on whose behalf the disk will be mounted, `mount_point`, `/path/to/blobfusetmp`,`/path/to/connection.cfg`):
+
+    ```bash
+    #!/bin/bash
+    sudo -u <user_name> blobfuse <mount_point> --tmp-path=/path/to/blobfusetmp  --config-file=/path/to/connection.cfg -o allow_other
+    exit 0
+    ```
+
+1.  Give it the execution permission:
+    ```bash
+    sudo chmod +x /usr/bin/azure_fuse
+    ```
+
+1.  Edit `/etc/fstab` with the blobfuse script. Add the following line(replace paths):
+```bash
+/absolute/path/to/azure_fuse </path/to/desired/mountpoint> fuse allow_other,user,_netdev
+```
+
+##### <a name="azure_using_systemd">Using systemd</a>
+
+1.  Create unit file `sudo nano /etc/systemd/system/blobfuse.service`.
+    (replace `user_name`, `mount_point`, `container_name`,`/path/to/connection.cfg`):
+
+    ```bash
+    [Unit]
+    Description=FUSE filesystem over Azure container
+    After=network.target
+
+    [Service]
+    Environment="MOUNT_POINT=<mount_point>"
+    User=<user_name>
+    Group=<user_name>
+    ExecStart=blobfuse ${MOUNT_POINT} --container-name=<container_name> --tmp-path=/mnt/blobfusetmp --config-file=/path/to/connection.cfg -o allow_other
+    ExecStop=fusermount -u ${MOUNT_POINT}
+    Restart=always
+    Type=forking
+
+    [Install]
+    WantedBy=multi-user.target
+    ```
+
+1.  Update the system configurations, enable unit autorun when the system boots, mount the container:
+
+    ```bash
+    sudo systemctl daemon-reload
+    sudo systemctl enable blobfuse.service
+    sudo systemctl start blobfuse.service
+    ```
+    Or for more detail [see here](https://github.com/Azure/azure-storage-fuse/tree/master/systemd)
+
+#### <a name="azure_check">Check</a>
+A file `/etc/mtab` contains records of currently mounted filesystems.
+```bash
+cat /etc/mtab | grep 'blobfuse'
+```
+
+#### <a name="azure_unmount_filesystem">Unmount filesystem</a>
+```bash
+fusermount -u <mount_point>
+```
+
+If you used [systemd](#azure_using_systemd) to mount a container:
+
+```bash
+sudo systemctl stop blobfuse.service
+sudo systemctl disable blobfuse.service
+```
+
+If you have any mounting problems, check out the [answers](https://github.com/Azure/azure-storage-fuse/wiki/3.-Troubleshoot-FAQ)
+to common problems
+
+## Google Drive as filesystem
+### <a name="google_drive_ubuntu_2004">Ubuntu 20.04</a>
+#### <a name="google_drive_mount">Mount</a>
+To mount a google drive as a filesystem in user space(FUSE)
+you can use [google-drive-ocamlfuse](https://github.com/astrada/google-drive-ocamlfuse)
+To do this follow the instructions below:
+
+1.  Install google-drive-ocamlfuse:
+
+    ```bash
+    sudo add-apt-repository ppa:alessandro-strada/ppa
+    sudo apt-get update
+    sudo apt-get install google-drive-ocamlfuse
+    ```
+
+1.  Run `google-drive-ocamlfuse` without parameters:
+
+    ```bash
+    google-drive-ocamlfuse
+    ```
+
+    This command will create the default application directory (~/.gdfuse/default),
+    containing the configuration file config (see the [wiki](https://github.com/astrada/google-drive-ocamlfuse/wiki)
+    page for more details about configuration).
+    And it will start a web browser to obtain authorization to access your Google Drive.
+    This will let you modify default configuration before mounting the filesystem.
+
+    Then you can choose a local directory to mount your Google Drive (e.g.: ~/GoogleDrive).
+
+1.  Create the mount point, if it doesn't exist(replace mount_point):
+
+    ```bash
+    mountpoint="<mount_point>"
+    mkdir -p $mountpoint
+    ```
+
+1.  Uncomment `user_allow_other` in the `/etc/fuse.conf` file: `sudo nano /etc/fuse.conf`
+1.  Mount the filesystem:
+
+    ```bash
+    google-drive-ocamlfuse -o allow_other $mountpoint
+    ```
+
+#### <a name="google_drive_automatically_mount">Automatically mount</a>
+Follow the first 4 mounting steps above.
+##### <a name="google_drive_using_fstab">Using fstab</a>
+
+1.  Create a bash script named gdfuse(e.g in /usr/bin, as root) with this content
+    (replace `user_name` on whose behalf the disk will be mounted, `label`, `mount_point`):
+
+    ```bash
+    #!/bin/bash
+    sudo -u <user_name> google-drive-ocamlfuse -o allow_other -label <label> <mount_point>
+    exit 0
+    ```
+
+1.  Give it the execution permission:
+
+    ```bash
+    sudo chmod +x /usr/bin/gdfuse
+    ```
+
+1.  Edit `/etc/fstab` adding a line like this, replace `mount_point`):
+
+    ```bash
+    /absolute/path/to/gdfuse  <mount_point>     fuse    allow_other,user,_netdev     0       0
+    ```
+
+    For more details see [here](https://github.com/astrada/google-drive-ocamlfuse/wiki/Automounting)
+
+##### <a name="google_drive_using_systemd">Using systemd</a>
+
+1.  Create unit file `sudo nano /etc/systemd/system/google-drive-ocamlfuse.service`.
+    (replace `user_name`, `label`(default `label=default`), `mount_point`):
+
+    ```bash
+    [Unit]
+    Description=FUSE filesystem over Google Drive
+    After=network.target
+
+    [Service]
+    Environment="MOUNT_POINT=<mount_point>"
+    User=<user_name>
+    Group=<user_name>
+    ExecStart=google-drive-ocamlfuse -label <label> ${MOUNT_POINT}
+    ExecStop=fusermount -u ${MOUNT_POINT}
+    Restart=always
+    Type=forking
+
+    [Install]
+    WantedBy=multi-user.target
+    ```
+
+1.  Update the system configurations, enable unit autorun when the system boots, mount the drive:
+
+    ```bash
+    sudo systemctl daemon-reload
+    sudo systemctl enable google-drive-ocamlfuse.service
+    sudo systemctl start google-drive-ocamlfuse.service
+    ```
+
+    For more details see [here](https://github.com/astrada/google-drive-ocamlfuse/wiki/Automounting)
+
+#### <a name="google_drive_check">Check</a>
+A file `/etc/mtab` contains records of currently mounted filesystems.
+```bash
+cat /etc/mtab | grep 'google-drive-ocamlfuse'
+```
+
+#### <a name="google_drive_unmount_filesystem">Unmount filesystem</a>
+```bash
+fusermount -u <mount_point>
+```
+
+If you used [systemd](#google_drive_using_systemd) to mount a drive:
+
+```bash
+sudo systemctl stop google-drive-ocamlfuse.service
+sudo systemctl disable google-drive-ocamlfuse.service
+```
diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py
index bfe79a4e18ee..4be31aeca850 100644
--- a/cvat/apps/engine/cache.py
+++ b/cvat/apps/engine/cache.py
@@ -10,9 +10,9 @@
 
 from cvat.apps.engine.media_extractors import (Mpeg4ChunkWriter,
     Mpeg4CompressedChunkWriter, ZipChunkWriter, ZipCompressedChunkWriter)
-from cvat.apps.engine.models import DataChoice
+from cvat.apps.engine.models import DataChoice, UploadedDataStorageLocationChoice
 from cvat.apps.engine.prepare import PrepareInfo
-
+from .log import slogger
 
 class CacheInteraction:
     def __init__(self):
@@ -31,28 +31,36 @@ def get_buff_mime(self, chunk_number, quality, db_data):
 
     def prepare_chunk_buff(self, db_data, quality, chunk_number):
         from cvat.apps.engine.frame_provider import FrameProvider # TODO: remove circular dependency
-        extractor_classes = {
+        writer_classes = {
             FrameProvider.Quality.COMPRESSED : Mpeg4CompressedChunkWriter if db_data.compressed_chunk_type == DataChoice.VIDEO else ZipCompressedChunkWriter,
             FrameProvider.Quality.ORIGINAL : Mpeg4ChunkWriter if db_data.original_chunk_type == DataChoice.VIDEO else ZipChunkWriter,
         }
 
-        image_quality = 100 if extractor_classes[quality] in [Mpeg4ChunkWriter, ZipChunkWriter] else db_data.image_quality
-        mime_type = 'video/mp4' if extractor_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'application/zip'
+        image_quality = 100 if writer_classes[quality] in [Mpeg4ChunkWriter, ZipChunkWriter] else db_data.image_quality
+        mime_type = 'video/mp4' if writer_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'application/zip'
 
-        extractor = extractor_classes[quality](image_quality)
+        writer = writer_classes[quality](image_quality)
 
         images = []
         buff = BytesIO()
-        if os.path.exists(db_data.get_meta_path()):
-            source_path = os.path.join(db_data.get_upload_dirname(), db_data.video.path)
-            meta = PrepareInfo(source_path=source_path, meta_path=db_data.get_meta_path())
-            for frame in meta.decode_needed_frames(chunk_number, db_data):
-                images.append(frame)
-            extractor.save_as_chunk([(image, source_path, None) for image in images], buff)
-        else:
-            with open(db_data.get_dummy_chunk_path(chunk_number), 'r') as dummy_file:
-                images = [os.path.join(db_data.get_upload_dirname(), line.strip()) for line in dummy_file]
-            extractor.save_as_chunk([(image, image, None) for image in images], buff)
+        upload_dir = {
+                UploadedDataStorageLocationChoice.LOCAL: db_data.get_upload_dirname(),
+                UploadedDataStorageLocationChoice.SHARE: settings.SHARE_ROOT
+            }[db_data.uploaded_data_storage_location]
+        try:
+            if os.path.exists(db_data.get_meta_path()):
+                source_path = os.path.join(upload_dir, db_data.video.path)
+                meta = PrepareInfo(source_path=source_path, meta_path=db_data.get_meta_path())
+                for frame in meta.decode_needed_frames(chunk_number, db_data):
+                    images.append(frame)
+                writer.save_as_chunk([(image, source_path, None) for image in images], buff)
+            else:
+                with open(db_data.get_dummy_chunk_path(chunk_number), 'r') as dummy_file:
+                    images = [os.path.join(upload_dir, line.strip()) for line in dummy_file]
+                writer.save_as_chunk([(image, image, None) for image in images], buff)
+        except FileNotFoundError as ex:
+            slogger.glob.exception(f"{ex.strerror} {ex.filename}")
+
         buff.seek(0)
         return buff, mime_type
 
diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py
index b58bf98c4cfd..a83fb6bb5399 100644
--- a/cvat/apps/engine/media_extractors.py
+++ b/cvat/apps/engine/media_extractors.py
@@ -135,10 +135,12 @@ def __init__(self, source_path, step=1, start=0, stop=None):
 class ArchiveReader(DirectoryReader):
     def __init__(self, source_path, step=1, start=0, stop=None):
         self._archive_source = source_path[0]
-        Archive(self._archive_source).extractall(os.path.dirname(source_path[0]))
-        os.remove(self._archive_source)
+        extract_dir = source_path[1] if len(source_path) > 1 else os.path.dirname(source_path[0])
+        Archive(self._archive_source).extractall(extract_dir)
+        if extract_dir == os.path.dirname(source_path[0]):
+            os.remove(self._archive_source)
         super().__init__(
-            source_path=[os.path.dirname(source_path[0])],
+            source_path=[extract_dir],
             step=step,
             start=start,
             stop=stop,
@@ -178,6 +180,7 @@ def _make_name():
 class ZipReader(ImageListReader):
     def __init__(self, source_path, step=1, start=0, stop=None):
         self._zip_source = zipfile.ZipFile(source_path[0], mode='r')
+        self.extract_dir = source_path[1] if len(source_path) > 1 else None
         file_list = [f for f in self._zip_source.namelist() if get_mime(f) == 'image']
         super().__init__(file_list, step, start, stop)
 
@@ -197,13 +200,15 @@ def get_image(self, i):
 
     def get_path(self, i):
         if  self._zip_source.filename:
-            return os.path.join(os.path.dirname(self._zip_source.filename), self._source_path[i])
+            return os.path.join(os.path.dirname(self._zip_source.filename), self._source_path[i]) \
+                if not self.extract_dir else os.path.join(self.extract_dir, self._source_path[i])
         else: # necessary for mime_type definition
             return self._source_path[i]
 
     def extract(self):
-        self._zip_source.extractall(os.path.dirname(self._zip_source.filename))
-        os.remove(self._zip_source.filename)
+        self._zip_source.extractall(self.extract_dir if self.extract_dir else os.path.dirname(self._zip_source.filename))
+        if not self.extract_dir:
+            os.remove(self._zip_source.filename)
 
 class VideoReader(IMediaReader):
     def __init__(self, source_path, step=1, start=0, stop=None):
diff --git a/cvat/apps/engine/migrations/0033_data_uploaded_data_storage_location.py b/cvat/apps/engine/migrations/0033_data_uploaded_data_storage_location.py
new file mode 100644
index 000000000000..84caba3e7fdb
--- /dev/null
+++ b/cvat/apps/engine/migrations/0033_data_uploaded_data_storage_location.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.1 on 2020-10-22 09:29
+
+import cvat.apps.engine.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('engine', '0032_remove_task_z_order'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='data',
+            name='uploaded_data_storage_location',
+            field=models.CharField(
+                choices=[('local', 'LOCAL'), ('share', 'SHARE')],
+                default=cvat.apps.engine.models.UploadedDataStorageLocationChoice['LOCAL'],
+                max_length=15
+            ),
+        ),
+    ]
diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py
index d99181bcc97f..92ab5dfbe5b5 100644
--- a/cvat/apps/engine/models.py
+++ b/cvat/apps/engine/models.py
@@ -54,6 +54,18 @@ def choices(cls):
     def __str__(self):
         return self.value
 
+class UploadedDataStorageLocationChoice(str, Enum):
+    #AWS_S3 = 'aws_s3_bucket'
+    LOCAL = 'local'
+    SHARE = 'share'
+
+    @classmethod
+    def choices(cls):
+        return tuple((x.value, x.name) for x in cls)
+
+    def __str__(self):
+        return self.value
+
 class Data(models.Model):
     chunk_size = models.PositiveIntegerField(null=True)
     size = models.PositiveIntegerField(default=0)
@@ -66,6 +78,8 @@ class Data(models.Model):
     original_chunk_type = models.CharField(max_length=32, choices=DataChoice.choices(),
         default=DataChoice.IMAGESET)
     storage_method = models.CharField(max_length=15, choices=StorageMethodChoice.choices(), default=StorageMethodChoice.FILE_SYSTEM)
+    uploaded_data_storage_location = models.CharField(max_length=15, choices=UploadedDataStorageLocationChoice.choices(),
+                                     default=UploadedDataStorageLocationChoice.LOCAL)
 
     class Meta:
         default_permissions = ()
diff --git a/cvat/apps/engine/prepare.py b/cvat/apps/engine/prepare.py
index 9ee546309142..a4fef43e96d1 100644
--- a/cvat/apps/engine/prepare.py
+++ b/cvat/apps/engine/prepare.py
@@ -180,8 +180,10 @@ def decode_needed_frames(self, chunk_number, db_data):
 class UploadedMeta(PrepareInfo):
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
+        uploaded_meta = kwargs.get('uploaded_meta')
+        assert uploaded_meta is not None , 'No uploaded meta path'
 
-        with open(self.meta_path, 'r') as meta_file:
+        with open(uploaded_meta, 'r') as meta_file:
             lines = meta_file.read().strip().split('\n')
             self.frames = int(lines.pop())
 
diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py
index 71843878313f..99b38676a36e 100644
--- a/cvat/apps/engine/serializers.py
+++ b/cvat/apps/engine/serializers.py
@@ -171,12 +171,13 @@ class DataSerializer(serializers.ModelSerializer):
     server_files = ServerFileSerializer(many=True, default=[])
     remote_files = RemoteFileSerializer(many=True, default=[])
     use_cache = serializers.BooleanField(default=False)
+    copy_data = serializers.BooleanField(default=False)
 
     class Meta:
         model = models.Data
         fields = ('chunk_size', 'size', 'image_quality', 'start_frame', 'stop_frame', 'frame_filter',
             'compressed_chunk_type', 'original_chunk_type', 'client_files', 'server_files', 'remote_files', 'use_zip_chunks',
-            'use_cache')
+            'use_cache', 'copy_data')
 
     # pylint: disable=no-self-use
     def validate_frame_filter(self, value):
@@ -205,6 +206,7 @@ def create(self, validated_data):
         remote_files = validated_data.pop('remote_files')
         validated_data.pop('use_zip_chunks')
         validated_data.pop('use_cache')
+        validated_data.pop('copy_data')
         db_data = models.Data.objects.create(**validated_data)
 
         data_path = db_data.get_data_dirname()
diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py
index e724d2425d39..e4220291036a 100644
--- a/cvat/apps/engine/task.py
+++ b/cvat/apps/engine/task.py
@@ -15,7 +15,7 @@
 from urllib import request as urlrequest
 
 from cvat.apps.engine.media_extractors import get_mime, MEDIA_TYPES, Mpeg4ChunkWriter, ZipChunkWriter, Mpeg4CompressedChunkWriter, ZipCompressedChunkWriter
-from cvat.apps.engine.models import DataChoice, StorageMethodChoice
+from cvat.apps.engine.models import DataChoice, StorageMethodChoice, UploadedDataStorageLocationChoice as LocationChoice
 from cvat.apps.engine.utils import av_scan_paths
 from cvat.apps.engine.prepare import prepare_meta
 
@@ -232,7 +232,10 @@ def _create_thread(tid, data):
             "File with meta information can be uploaded if 'Use cache' option is also selected"
 
     if data['server_files']:
-        _copy_data_from_share(data['server_files'], upload_dir)
+        if db_data.uploaded_data_storage_location == LocationChoice.LOCAL:
+            _copy_data_from_share(data['server_files'], upload_dir)
+        else:
+            upload_dir = settings.SHARE_ROOT
 
     av_scan_paths(upload_dir)
 
@@ -247,8 +250,11 @@ def _create_thread(tid, data):
         if media_files:
             if extractor is not None:
                 raise Exception('Combined data types are not supported')
+            source_paths=[os.path.join(upload_dir, f) for f in media_files]
+            if media_type in  ('archive', 'zip') and db_data.uploaded_data_storage_location == LocationChoice.SHARE:
+                source_paths.append(db_data.get_upload_dirname())
             extractor = MEDIA_TYPES[media_type]['extractor'](
-                source_path=[os.path.join(upload_dir, f) for f in media_files],
+                source_path=source_paths,
                 step=db_data.get_frame_step(),
                 start=db_data.start_frame,
                 stop=data['stop_frame'],
@@ -303,13 +309,9 @@ def update_progress(progress):
                     if meta_info_file:
                         try:
                             from cvat.apps.engine.prepare import UploadedMeta
-                            if os.path.split(meta_info_file[0])[0]:
-                                os.replace(
-                                    os.path.join(upload_dir, meta_info_file[0]),
-                                    db_data.get_meta_path()
-                                )
                             meta_info = UploadedMeta(source_path=os.path.join(upload_dir, media_files[0]),
-                                                     meta_path=db_data.get_meta_path())
+                                                     meta_path=db_data.get_meta_path(),
+                                                     uploaded_meta=os.path.join(upload_dir, meta_info_file[0]))
                             meta_info.check_seek_key_frames()
                             meta_info.check_frames_numbers()
                             meta_info.save_meta_info()
@@ -322,6 +324,7 @@ def update_progress(progress):
                             meta_info, smooth_decoding = prepare_meta(
                                 media_file=media_files[0],
                                 upload_dir=upload_dir,
+                                meta_dir=os.path.dirname(db_data.get_meta_path()),
                                 chunk_size=db_data.chunk_size
                             )
                             assert smooth_decoding == True, 'Too few keyframes for smooth video decoding.'
@@ -329,6 +332,7 @@ def update_progress(progress):
                         meta_info, smooth_decoding = prepare_meta(
                             media_file=media_files[0],
                             upload_dir=upload_dir,
+                            meta_dir=os.path.dirname(db_data.get_meta_path()),
                             chunk_size=db_data.chunk_size
                         )
                         assert smooth_decoding == True, 'Too few keyframes for smooth video decoding.'
diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py
index 446783e70372..5da5f38d347d 100644
--- a/cvat/apps/engine/tests/test_rest_api.py
+++ b/cvat/apps/engine/tests/test_rest_api.py
@@ -29,7 +29,7 @@
 from rest_framework.test import APIClient, APITestCase
 
 from cvat.apps.engine.models import (AttributeType, Data, Job, Project,
-    Segment, StatusChoice, Task, StorageMethodChoice)
+    Segment, StatusChoice, Task, StorageMethodChoice, UploadedDataStorageLocationChoice as StorageLocation)
 from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload
 
 def create_db_users(cls):
@@ -1649,7 +1649,8 @@ def _extract_video_chunk(chunk_buffer):
         return [f.to_image() for f in container.decode(stream)]
 
     def _test_api_v1_tasks_id_data_spec(self, user, spec, data, expected_compressed_type, expected_original_type, image_sizes,
-                                        expected_storage_method=StorageMethodChoice.FILE_SYSTEM):
+                                        expected_storage_method=StorageMethodChoice.FILE_SYSTEM,
+                                        expected_uploaded_data_location=StorageLocation.LOCAL):
         # create task
         response = self._create_task(user, spec)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@@ -1673,7 +1674,13 @@ def _test_api_v1_tasks_id_data_spec(self, user, spec, data, expected_compressed_
             self.assertEqual(expected_compressed_type, task["data_compressed_chunk_type"])
             self.assertEqual(expected_original_type, task["data_original_chunk_type"])
             self.assertEqual(len(image_sizes), task["size"])
-            self.assertEqual(expected_storage_method, Task.objects.get(pk=task_id).data.storage_method)
+            db_data = Task.objects.get(pk=task_id).data
+            self.assertEqual(expected_storage_method, db_data.storage_method)
+            self.assertEqual(expected_uploaded_data_location, db_data.uploaded_data_storage_location)
+            # check if used share without copying inside and files doesn`t exist in ../raw/
+            if expected_uploaded_data_location is StorageLocation.SHARE:
+                self.assertEqual(False,
+                    os.path.exists(os.path.join(db_data.get_upload_dirname(), next(iter(data.values())))))
 
         # check preview
         response = self._get_preview(task_id, user)
@@ -1767,7 +1774,7 @@ def _test_api_v1_tasks_id_data(self, user):
         self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET, image_sizes)
 
         task_spec = {
-            "name": "my task #2",
+            "name": "my task without copying #2",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -1790,10 +1797,16 @@ def _test_api_v1_tasks_id_data(self, user):
             self._image_sizes[task_data["server_files[2]"]],
         ]
 
-        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET, image_sizes)
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET, image_sizes,
+                                             expected_uploaded_data_location=StorageLocation.SHARE)
+
+        task_spec.update([('name', 'my task #3')])
+        task_data.update([('copy_data', True)])
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET,
+                                             image_sizes, expected_uploaded_data_location=StorageLocation.LOCAL)
 
         task_spec = {
-            "name": "my video task #1",
+            "name": "my video task #4",
             "overlap": 0,
             "segment_size": 100,
             "labels": [
@@ -1810,7 +1823,7 @@ def _test_api_v1_tasks_id_data(self, user):
         self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO, image_sizes)
 
         task_spec = {
-            "name": "my video task #2",
+            "name": "my video task without copying #5",
             "overlap": 0,
             "segment_size": 5,
             "labels": [
@@ -1825,10 +1838,16 @@ def _test_api_v1_tasks_id_data(self, user):
         }
         image_sizes = self._image_sizes[task_data["server_files[0]"]]
 
-        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO, image_sizes)
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO, image_sizes,
+                                             expected_uploaded_data_location=StorageLocation.SHARE)
+
+        task_spec.update([('name', 'my video task #6')])
+        task_data.update([('copy_data', True)])
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO,
+                                             image_sizes, expected_uploaded_data_location=StorageLocation.LOCAL)
 
         task_spec = {
-            "name": "my video task #3",
+            "name": "my video task without copying #7",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -1842,10 +1861,16 @@ def _test_api_v1_tasks_id_data(self, user):
         }
         image_sizes = self._image_sizes[task_data["server_files[0]"]]
 
-        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO, image_sizes)
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO, image_sizes,
+                                             expected_uploaded_data_location=StorageLocation.SHARE)
+
+        task_spec.update([("name", "my video task #8")])
+        task_data.update([("copy_data", True)])
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO,
+                                             image_sizes, expected_uploaded_data_location=StorageLocation.LOCAL)
 
         task_spec = {
-            "name": "my video task #4",
+            "name": "my video task without copying #9",
             "overlap": 0,
             "segment_size": 5,
             "labels": [
@@ -1861,10 +1886,16 @@ def _test_api_v1_tasks_id_data(self, user):
         }
         image_sizes = self._image_sizes[task_data["server_files[0]"]]
 
-        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.VIDEO, image_sizes)
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.VIDEO, image_sizes,
+                                             expected_uploaded_data_location=StorageLocation.SHARE)
+
+        task_spec.update([('name', 'my video task #10')])
+        task_data.update([('copy_data', True)])
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.VIDEO,
+                                             image_sizes, expected_uploaded_data_location=StorageLocation.LOCAL)
 
         task_spec = {
-            "name": "my archive task #6",
+            "name": "my archive task without copying #11",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -1878,10 +1909,16 @@ def _test_api_v1_tasks_id_data(self, user):
         }
         image_sizes = self._image_sizes[task_data["server_files[0]"]]
 
-        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET, image_sizes)
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET, image_sizes,
+                                             expected_uploaded_data_location=StorageLocation.SHARE)
+
+        task_spec.update([('name', 'my archive task #12')])
+        task_data.update([('copy_data', True)])
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET,
+                                             image_sizes, expected_uploaded_data_location=StorageLocation.LOCAL)
 
         task_spec = {
-            "name": "my archive task #7",
+            "name": "my archive task #13",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -1898,7 +1935,7 @@ def _test_api_v1_tasks_id_data(self, user):
         self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET, image_sizes)
 
         task_spec = {
-            "name": "cached video task #8",
+            "name": "cached video task without copying #14",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -1916,10 +1953,15 @@ def _test_api_v1_tasks_id_data(self, user):
         image_sizes = self._image_sizes[task_data["server_files[0]"]]
 
         self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO,
-            self.ChunkType.VIDEO, image_sizes, StorageMethodChoice.CACHE)
+            self.ChunkType.VIDEO, image_sizes, StorageMethodChoice.CACHE, StorageLocation.SHARE)
+
+        task_spec.update([('name', 'cached video task #15')])
+        task_data.update([('copy_data', True)])
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO,
+                                             image_sizes, StorageMethodChoice.CACHE, StorageLocation.LOCAL)
 
         task_spec = {
-            "name": "cached images task #9",
+            "name": "cached images task without copying #16",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -1942,10 +1984,15 @@ def _test_api_v1_tasks_id_data(self, user):
         ]
 
         self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET,
-            self.ChunkType.IMAGESET, image_sizes, StorageMethodChoice.CACHE)
+            self.ChunkType.IMAGESET, image_sizes, StorageMethodChoice.CACHE, StorageLocation.SHARE)
+
+        task_spec.update([('name', 'cached images task #17')])
+        task_data.update([('copy_data', True)])
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET,
+                                             image_sizes, StorageMethodChoice.CACHE, StorageLocation.LOCAL)
 
         task_spec = {
-            "name": "my cached zip archive task #10",
+            "name": "my cached zip archive task without copying #18",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -1963,10 +2010,15 @@ def _test_api_v1_tasks_id_data(self, user):
         image_sizes = self._image_sizes[task_data["server_files[0]"]]
 
         self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET,
-            self.ChunkType.IMAGESET, image_sizes, StorageMethodChoice.CACHE)
+            self.ChunkType.IMAGESET, image_sizes, StorageMethodChoice.CACHE, StorageLocation.SHARE)
+
+        task_spec.update([('name', 'my cached zip archive task #19')])
+        task_data.update([('copy_data', True)])
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET, self.ChunkType.IMAGESET,
+                                             image_sizes, StorageMethodChoice.CACHE, StorageLocation.LOCAL)
 
         task_spec = {
-            "name": "my cached pdf task #11",
+            "name": "my cached pdf task #20",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -1988,7 +2040,7 @@ def _test_api_v1_tasks_id_data(self, user):
             image_sizes, StorageMethodChoice.CACHE)
 
         task_spec = {
-            "name": "my pdf task #12",
+            "name": "my pdf task #21",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -2013,7 +2065,7 @@ def _test_api_v1_tasks_id_data(self, user):
             os.path.join(settings.SHARE_ROOT, "videos")
         )
         task_spec = {
-            "name": "my video with meta info task #13",
+            "name": "my video with meta info task without copying #22",
             "overlap": 0,
             "segment_size": 0,
             "labels": [
@@ -2030,7 +2082,13 @@ def _test_api_v1_tasks_id_data(self, user):
         image_sizes = self._image_sizes[task_data['server_files[0]']]
 
         self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO,
-                                            self.ChunkType.VIDEO, image_sizes, StorageMethodChoice.CACHE)
+                                            self.ChunkType.VIDEO, image_sizes, StorageMethodChoice.CACHE,
+                                            StorageLocation.SHARE)
+
+        task_spec.update([('name', 'my video with meta info task #23')])
+        task_data.update([('copy_data', True)])
+        self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO,
+                                             image_sizes, StorageMethodChoice.CACHE, StorageLocation.LOCAL)
 
         task_spec = {
             "name": "my cached video task #14",
diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py
index 50798c732c5a..251d0d99ba9a 100644
--- a/cvat/apps/engine/views.py
+++ b/cvat/apps/engine/views.py
@@ -36,7 +36,9 @@
 from cvat.apps.authentication import auth
 from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer
 from cvat.apps.engine.frame_provider import FrameProvider
-from cvat.apps.engine.models import Job, StatusChoice, Task, StorageMethodChoice
+from cvat.apps.engine.models import (
+    Job, StatusChoice, Task, StorageMethodChoice, UploadedDataStorageLocationChoice
+)
 from cvat.apps.engine.serializers import (
     AboutSerializer, AnnotationFileSerializer, BasicUserSerializer,
     DataMetaSerializer, DataSerializer, ExceptionSerializer,
@@ -395,10 +397,13 @@ def data(self, request, pk):
             data = {k:v for k, v in serializer.data.items()}
             data['use_zip_chunks'] = serializer.validated_data['use_zip_chunks']
             data['use_cache'] = serializer.validated_data['use_cache']
+            data['copy_data'] = serializer.validated_data['copy_data']
             if data['use_cache']:
                 db_task.data.storage_method = StorageMethodChoice.CACHE
                 db_task.data.save(update_fields=['storage_method'])
-
+            if data['server_files'] and data.get('copy_data') == False:
+                db_task.data.uploaded_data_storage_location = UploadedDataStorageLocationChoice.SHARE
+                db_task.data.save(update_fields=['uploaded_data_storage_location'])
             # if the value of stop_frame is 0, then inside the function we cannot know
             # the value specified by the user or it's default value from the database
             if 'stop_frame' not in serializer.validated_data:


Code review comments:
diff_hunk: @@ -42,6 +42,7 @@ const defaultState = {
         lfs: false,
         useZipChunks: true,
         useCache: true,
+        activeTab: 'local',
path: cvat-ui/src/components/create-task-page/create-task-content.tsx
text: @user1:
Let's push it out of the nested advanced object

diff_hunk: @@ -26,11 +26,14 @@ export interface AdvancedConfiguration {
     useZipChunks: boolean;
     dataChunkSize?: number;
     useCache: boolean;
+    activeTab: string;
+    copyData?: boolean;
 }
 
 type Props = FormComponentProps & {
     onSubmit(values: AdvancedConfiguration): void;
     installedGit: boolean;
+    activeTab: string;
path: cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx
text: @user1:
let's rename to ``activeFileManagerTab``

diff_hunk: @@ -26,11 +26,14 @@ export interface AdvancedConfiguration {
     useZipChunks: boolean;
     dataChunkSize?: number;
     useCache: boolean;
+    activeTab: string;
path: cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx
text: @user1:
activeTab shouldn't be a part of configuration. It is a prop.

diff_hunk: @@ -54,6 +54,18 @@ def choices(cls):
     def __str__(self):
         return self.value
 
+class UploadedDataStorageLocationChoice(str, Enum):
path: cvat/apps/engine/models.py
text: @user1:
I will vote for a shorter name like StorageChoice, DataLocationChoice or something like that.

@author:
Renamed