diff --git a/CHANGELOG.md b/CHANGELOG.md index 602b284de8e5..7023ec7b6749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Manual review pipeline: issues/comments/workspace () - Added basic projects implementation () +- Added documentation on how to mount cloud starage(AWS S3 bucket, Azure container, Google Drive) as FUSE () +- Added ability to work with share files without copying inside () ### Changed diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index c7fe66d54b5d..126dc8857669 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -188,6 +188,7 @@ Standard JS events are used. - canvas.roiselected => {points: number[]} - canvas.resizeshape => {id: number} - canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number } + - canvas.error => { exception: Error } ``` ### WEB diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index f3860893ac92..4a30e6185c6e 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -133,6 +133,7 @@ export enum UpdateReasons { DRAG_CANVAS = 'drag_canvas', ZOOM_CANVAS = 'zoom_canvas', CONFIG_UPDATED = 'config_updated', + DATA_FAILED = 'data_failed', } export enum Mode { @@ -168,6 +169,7 @@ export interface CanvasModel { readonly selected: any; geometry: Geometry; mode: Mode; + exception: Error | null; zoom(x: number, y: number, direction: number): void; move(topOffset: number, leftOffset: number): void; @@ -224,6 +226,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { splitData: SplitData; selected: any; mode: Mode; + exception: Error | null; }; public constructor() { @@ -284,6 +287,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { }, selected: null, mode: Mode.IDLE, + exception: null, }; } @@ -411,6 +415,8 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.notify(UpdateReasons.OBJECTS_UPDATED); }) .catch((exception: any): void => { + this.data.exception = exception; + this.notify(UpdateReasons.DATA_FAILED); throw exception; }); } @@ -743,4 +749,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { public get mode(): Mode { return this.data.mode; } + public get exception(): Error { + return this.data.exception; + } } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 32c962098465..ddec7d158064 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1343,6 +1343,14 @@ export class CanvasViewImpl implements CanvasView, Listener { this.mode = Mode.IDLE; this.canvas.style.cursor = ''; } + else if (reason === UpdateReasons.DATA_FAILED) { + const event: CustomEvent = new CustomEvent('canvas.error', { + detail: { + exception: model.exception, + }, + }); + this.canvas.dispatchEvent(event); + } if (model.imageBitmap && [UpdateReasons.IMAGE_CHANGED, UpdateReasons.OBJECTS_UPDATED].includes(reason)) { this.redrawBitmap(); diff --git a/cvat-core/src/download.worker.js b/cvat-core/src/download.worker.js index 027ba87a7820..35d899d2cf28 100644 --- a/cvat-core/src/download.worker.js +++ b/cvat-core/src/download.worker.js @@ -20,7 +20,9 @@ onmessage = (e) => { .catch((error) => { postMessage({ id: e.data.id, - error, + error: error, + status: error.response.status, + responseData: error.response.data, isSuccess: false, }); }); diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 3df795eb5deb..7865df667b0b 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -31,7 +31,13 @@ if (e.data.isSuccess) { requests[e.data.id].resolve(e.data.responseData); } else { - requests[e.data.id].reject(e.data.error); + requests[e.data.id].reject({ + error: e.data.error, + response: { + status: e.data.status, + data: e.data.responseData, + }, + }); } delete requests[e.data.id]; @@ -725,7 +731,14 @@ }, ); } catch (errorData) { - throw generateError(errorData); + throw generateError({ + ...errorData, + message: '', + response: { + ...errorData.response, + data: String.fromCharCode.apply(null, new Uint8Array(errorData.response.data)), + }, + }); } return response; diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index d6291f0535a5..19bc32df6014 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -973,6 +973,7 @@ data_original_chunk_type: undefined, use_zip_chunks: undefined, use_cache: undefined, + copy_data: undefined, }; let updatedFields = { @@ -1239,6 +1240,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 @@ -1908,6 +1925,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/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index fdf86a7f2d07..8f486e96d48d 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -185,6 +185,7 @@ export enum AnnotationActionTypes { SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED', INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS', SET_AI_TOOLS_REF = 'SET_AI_TOOLS_REF', + GET_DATA_FAILED = 'GET_DATA_FAILED', SWITCH_REQUEST_REVIEW_DIALOG = 'SWITCH_REQUEST_REVIEW_DIALOG', SWITCH_SUBMIT_REVIEW_DIALOG = 'SWITCH_SUBMIT_REVIEW_DIALOG', SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG = 'SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG', @@ -218,6 +219,15 @@ export function changeWorkspace(workspace: Workspace): AnyAction { }; } +export function getDataFailed(error: any): AnyAction { + return { + type: AnnotationActionTypes.GET_DATA_FAILED, + payload: { + error, + }, + }; +} + export function addZLayer(): AnyAction { return { type: AnnotationActionTypes.ADD_Z_LAYER, @@ -913,7 +923,16 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init const frameData = await job.frames.get(frameNumber); // call first getting of frame data before rendering interface // to load and decode first chunk - await frameData.data(); + try { + await frameData.data(); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.GET_DATA_FAILED, + payload: { + error, + }, + }); + } const states = await job.annotations.get(frameNumber, showAllInterpolationTracks, filters); const issues = await job.issues(); const reviews = await job.reviews(); diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 349cd880f034..2be26d82d5bb 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -394,6 +394,9 @@ export function createTaskAsync(data: any): ThunkAction, {}, {}, 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/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx index c2fac4461ef8..20007713d882 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx @@ -90,6 +90,7 @@ interface Props { onSwitchGrid(enabled: boolean): void; onSwitchAutomaticBordering(enabled: boolean): void; onFetchAnnotation(): void; + onGetDataFailed(error: any): void; onStartIssue(position: number[]): void; } @@ -322,10 +323,17 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted); canvasInstance.html().removeEventListener('canvas.contextmenu', this.onCanvasPointContextMenu); + canvasInstance.html().removeEventListener('canvas.error', this.onCanvasErrorOccurrence); window.removeEventListener('resize', this.fitCanvas); } + private onCanvasErrorOccurrence = (event: any): void => { + const { exception } = event.detail; + const { onGetDataFailed } = this.props; + onGetDataFailed(exception); + }; + private onCanvasShapeDrawn = (event: any): void => { const { jobInstance, activeLabelID, activeObjectType, frame, onShapeDrawn, onCreateAnnotations, @@ -745,6 +753,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted); canvasInstance.html().addEventListener('canvas.contextmenu', this.onCanvasPointContextMenu); + canvasInstance.html().addEventListener('canvas.error', this.onCanvasErrorOccurrence); } public render(): JSX.Element { 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..fc12853ef12a 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,13 @@ export interface AdvancedConfiguration { useZipChunks: boolean; dataChunkSize?: number; useCache: boolean; + copyData?: boolean; } type Props = FormComponentProps & { onSubmit(values: AdvancedConfiguration): void; installedGit: boolean; + activeFileManagerTab: string; }; function isPositiveInteger(_: any, value: any, callback: any): void { @@ -114,6 +116,26 @@ class AdvancedConfigurationForm extends React.PureComponent { form.resetFields(); } + renderCopyDataChechbox(): JSX.Element { + const { form } = this.props; + return ( + + + + {form.getFieldDecorator('copyData', { + initialValue: false, + valuePropName: 'checked', + })( + + Copy data into CVAT + , + )} + + + + ); + } + private renderImageQuality(): JSX.Element { const { form } = this.props; @@ -386,10 +408,12 @@ class AdvancedConfigurationForm extends React.PureComponent { } public render(): JSX.Element { - const { installedGit } = this.props; - + const { installedGit, activeFileManagerTab } = this.props; return (
+ + {activeFileManagerTab === 'share' ? this.renderCopyDataChechbox() : null} + {this.renderUzeZipChunks()} 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 2fcaa247945d..7c76aba4aa8c 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 @@ -25,6 +25,7 @@ export interface CreateTaskData { advanced: AdvancedConfiguration; labels: any[]; files: Files; + activeFileManagerTab: string; } interface Props { @@ -53,6 +54,7 @@ const defaultState = { share: [], remote: [], }, + activeFileManagerTab: 'local', }; class CreateTaskContent extends React.PureComponent { @@ -132,6 +134,14 @@ class CreateTaskContent extends React.PureComponent { + const values = this.state; + this.setState({ + ...values, + activeFileManagerTab: key + }); + }; + private handleSubmitClick = (): void => { if (!this.validateLabelsOrProject()) { notification.error({ @@ -238,6 +248,7 @@ class CreateTaskContent extends React.PureComponent* Select files: { this.fileManagerContainer = container; }} @@ -255,6 +266,7 @@ class CreateTaskContent extends React.PureComponentAdvanced configuration}> { 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 { @@ -215,7 +216,7 @@ export default class FileManager extends React.PureComponent { } 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 { 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/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx index 5bbb191c925d..ef8b5dc39f94 100644 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx @@ -27,6 +27,7 @@ import { addZLayer, switchZLayer, fetchAnnotationsAsync, + getDataFailed, } from 'actions/annotation-actions'; import { switchGrid, @@ -121,6 +122,7 @@ interface DispatchToProps { onSwitchGrid(enabled: boolean): void; onSwitchAutomaticBordering(enabled: boolean): void; onFetchAnnotation(): void; + onGetDataFailed(error: any): void; onStartIssue(position: number[]): void; } @@ -308,6 +310,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onFetchAnnotation(): void { dispatch(fetchAnnotationsAsync()); }, + onGetDataFailed(error: any): void { + dispatch(getDataFailed(error)); + }, onStartIssue(position: number[]): void { dispatch(reviewActions.startIssue(position)); }, 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 { } public render(): JSX.Element { - const { treeData, getTreeData, withRemote } = this.props; + const { treeData, getTreeData, withRemote, onChangeActiveKey } = this.props; return ( { this.managerComponentRef = component; diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 1049eb54c5a7..7780eb9d3cc1 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -192,6 +192,18 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } + case AnnotationActionTypes.GET_DATA_FAILED: { + return { + ...state, + player: { + ...state.player, + frame: { + ...state.player.frame, + fetching: false, + }, + }, + } + } case AnnotationActionTypes.CHANGE_FRAME: { return { ...state, diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 44f7b59876e8..9f2caec00024 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -1076,6 +1076,21 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case AnnotationActionTypes.GET_DATA_FAILED: { + return { + ...state, + errors: { + ...state.errors, + annotation: { + ...state.errors.annotation, + jobFetching: { + message: 'Could not fetch frame data from the server', + reason: action.payload.error, + }, + }, + }, + }; + } case BoundariesActionTypes.RESET_AFTER_ERROR: case AuthActionTypes.LOGOUT_SUCCESS: { return { ...defaultState }; 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 +### Ubuntu 20.04 +#### Mount + +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 -o allow_other + ``` + +For more details see [here](https://github.com/s3fs-fuse/s3fs-fuse). + +#### Automatically mount +Follow the first 3 mounting steps above. + +##### Using fstab + +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 s3fs -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 fuse allow_other,user,_netdev 0 0 + ``` + +##### Using systemd + +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=" + User= + Group= + ExecStart=s3fs ${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 + ``` + +#### Check +A file `/etc/mtab` contains records of currently mounted filesystems. +```bash +cat /etc/mtab | grep 's3fs' +``` + +#### Unmount filesystem +```bash +fusermount -u +``` + +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 +### Ubuntu 20.04 +#### Mount +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= + export AZURE_STORAGE_ACCESS_KEY= + 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 /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= --tmp-path=/mnt/blobfusetmp -o allow_other + ``` + +#### Automatically mount +Follow the first 7 mounting steps above. +##### Using fstab + +1. Create configuration file `connection.cfg` with same content, change accountName, + select one from accountKey or sasToken and replace with your value: + + ```bash + accountName + # Please provide either an account key or a SAS token, and delete the other line. + accountKey + #change authType to specify only 1 + sasToken + authType + containerName + ``` + +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 blobfuse --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 fuse allow_other,user,_netdev +``` + +##### Using systemd + +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=" + User= + Group= + ExecStart=blobfuse ${MOUNT_POINT} --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) + +#### Check +A file `/etc/mtab` contains records of currently mounted filesystems. +```bash +cat /etc/mtab | grep 'blobfuse' +``` + +#### Unmount filesystem +```bash +fusermount -u +``` + +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 +### Ubuntu 20.04 +#### Mount +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="" + 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 + ``` + +#### Automatically mount +Follow the first 4 mounting steps above. +##### Using fstab + +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 google-drive-ocamlfuse -o allow_other -label