Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project backups #3852

Merged
merged 22 commits into from
Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add a tutorial on attaching cloud storage AWS-S3 (<https://github.com/openvinotoolkit/cvat/pull/3745>)
and Azure Blob Container (<https://github.com/openvinotoolkit/cvat/pull/3778>)
- The feature to remove annotations in a specified range of frames (<https://github.com/openvinotoolkit/cvat/pull/3617>)
- Project backup/restore (<https://github.com/openvinotoolkit/cvat/pull/3852>)

### Changed

Expand Down
4 changes: 2 additions & 2 deletions cvat-core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.20.1",
"version": "3.21.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
Expand Down
10 changes: 10 additions & 0 deletions cvat-core/src/project-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@
return result;
};

projectClass.prototype.export.implementation = async function () {
const result = await serverProxy.projects.exportProject(this.id);
return result;
};

projectClass.import.implementation = async function (file) {
azhavoro marked this conversation as resolved.
Show resolved Hide resolved
const result = await serverProxy.projects.importProject(file);
return result.id;
};

return projectClass;
}

Expand Down
30 changes: 30 additions & 0 deletions cvat-core/src/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,36 @@
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete);
return result;
}

/**
* Method makes a backup of a project
* @method export
* @memberof module:API.cvat.classes.Project
* @readonly
azhavoro marked this conversation as resolved.
Show resolved Hide resolved
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async export() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.export);
return result;
}

/**
* Method imports a project from a backup
* @method import
* @memberof module:API.cvat.classes.Project
* @readonly
azhavoro marked this conversation as resolved.
Show resolved Hide resolved
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
static async import(file) {
const result = await PluginRegistry.apiWrapper.call(this, Project.import, file);
return result;
}
}

Object.defineProperties(
Expand Down
61 changes: 58 additions & 3 deletions cvat-core/src/server-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,12 +515,12 @@

async function exportTask(id) {
const { backendAPI } = config;
const url = `${backendAPI}/tasks/${id}`;
const url = `${backendAPI}/tasks/${id}/backup`;

return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios.get(`${url}?action=export`, {
const response = await Axios.get(url, {
proxy: config.proxy,
});
if (response.status === 202) {
Expand All @@ -546,7 +546,7 @@
return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios.post(`${backendAPI}/tasks?action=import`, taskData, {
const response = await Axios.post(`${backendAPI}/tasks/backup`, taskData, {
proxy: config.proxy,
});
if (response.status === 202) {
Expand All @@ -566,6 +566,59 @@
});
}

async function exportProject(id) {
const { backendAPI } = config;
const url = `${backendAPI}/projects/${id}/backup`;

return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios.get(url, {
proxy: config.proxy,
});
if (response.status === 202) {
setTimeout(request, 3000);
} else {
resolve(`${url}?action=download`);
}
} catch (errorData) {
reject(generateError(errorData));
}
}

setTimeout(request);
});
}

async function importProject(file) {
const { backendAPI } = config;

let taskData = new FormData();
taskData.append('project_file', file);

return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios.post(`${backendAPI}/projects/backup`, taskData, {
proxy: config.proxy,
});
if (response.status === 202) {
taskData = new FormData();
taskData.append('rq_id', response.data.rq_id);
setTimeout(request, 3000);
} else {
const importedProject = await getProjects(`?id=${response.data.id}`);
resolve(importedProject[0]);
}
} catch (errorData) {
reject(generateError(errorData));
}
}

setTimeout(request);
});
}

async function createTask(taskSpec, taskDataSpec, onUpdate) {
const { backendAPI } = config;

Expand Down Expand Up @@ -1343,6 +1396,8 @@
create: createProject,
delete: deleteProject,
exportDataset: exportDataset('projects'),
exportProject,
importProject,
}),
writable: false,
},
Expand Down
4 changes: 2 additions & 2 deletions cvat-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.28.1",
"version": "1.29.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
Expand Down
48 changes: 48 additions & 0 deletions cvat-ui/src/actions/projects-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export enum ProjectsActionTypes {
DELETE_PROJECT = 'DELETE_PROJECT',
DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS',
DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED',
EXPORT_PROJECT = 'EXPORT_PROJECT',
EXPORT_PROJECT_SUCCESS = 'EXPORT_PROJECT_SUCCESS',
EXPORT_PROJECT_FAILED = 'EXPORT_PROJECT_FAILED',
IMPORT_PROJECT = 'IMPORT_PROJECT',
IMPORT_PROJECT_SUCCESS = 'IMPORT_PROJECT_SUCCESS',
IMPORT_PROJECT_FAILED = 'IMPORT_PROJECT_FAILED',
}

// prettier-ignore
Expand Down Expand Up @@ -55,6 +61,20 @@ const projectActions = {
deleteProjectFailed: (projectId: number, error: any) => (
createAction(ProjectsActionTypes.DELETE_PROJECT_FAILED, { projectId, error })
),
exportProject: (projectId: number) => createAction(ProjectsActionTypes.EXPORT_PROJECT, { projectId }),
exportProjectSuccess: (projectID: number) => (
createAction(ProjectsActionTypes.EXPORT_PROJECT_SUCCESS, { projectID })
),
exportProjectFailed: (projectID: number, error: any) => (
createAction(ProjectsActionTypes.EXPORT_PROJECT_FAILED, { projectId: projectID, error })
),
importProject: () => createAction(ProjectsActionTypes.IMPORT_PROJECT),
importProjectSuccess: (projectID: number) => (
createAction(ProjectsActionTypes.IMPORT_PROJECT_SUCCESS, { projectID })
),
importProjectFailed: (error: any) => (
createAction(ProjectsActionTypes.IMPORT_PROJECT_FAILED, { error })
),
};

export type ProjectActions = ActionUnion<typeof projectActions>;
Expand Down Expand Up @@ -163,3 +183,31 @@ export function deleteProjectAsync(projectInstance: any): ThunkAction {
}
};
}

export function importProjectAsync(file: File): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(projectActions.importProject());
try {
const projectInstance = await cvat.classes.Project.import(file);
dispatch(projectActions.importProjectSuccess(projectInstance));
} catch (error) {
dispatch(projectActions.importProjectFailed(error));
}
};
}

export function exportProjectAsync(projectInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(projectActions.exportProject(projectInstance.id));

try {
const url = await projectInstance.export();
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.click();
dispatch(projectActions.exportProjectSuccess(projectInstance.id));
} catch (error) {
dispatch(projectActions.exportProjectFailed(projectInstance.id, error));
}
};
}
51 changes: 27 additions & 24 deletions cvat-ui/src/components/actions-menu/actions-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT

import './styles.scss';
import React from 'react';
import React, { useCallback } from 'react';
import Menu from 'antd/lib/menu';
import Modal from 'antd/lib/modal';
import { LoadingOutlined } from '@ant-design/icons';
Expand Down Expand Up @@ -50,29 +50,32 @@ function ActionsMenuComponent(props: Props): JSX.Element {
exportIsActive,
} = props;

function onClickMenuWrapper(params: MenuInfo): void {
if (!params) {
return;
}
const onClickMenuWrapper = useCallback(
(params: MenuInfo) => {
if (!params) {
return;
}

if (params.key === Actions.DELETE_TASK) {
Modal.confirm({
title: `The task ${taskID} will be deleted`,
content: 'All related data (images, annotations) will be lost. Continue?',
className: 'cvat-modal-confirm-delete-task',
onOk: () => {
onClickMenu(params);
},
okButtonProps: {
type: 'primary',
danger: true,
},
okText: 'Delete',
});
} else {
onClickMenu(params);
}
}
if (params.key === Actions.DELETE_TASK) {
Modal.confirm({
title: `The task ${taskID} will be deleted`,
azhavoro marked this conversation as resolved.
Show resolved Hide resolved
content: 'All related data (images, annotations) will be lost. Continue?',
className: 'cvat-modal-confirm-delete-task',
onOk: () => {
onClickMenu(params);
},
okButtonProps: {
type: 'primary',
danger: true,
},
okText: 'Delete',
});
} else {
onClickMenu(params);
}
},
[],
);

return (
<Menu selectable={false} className='cvat-actions-menu' onClick={onClickMenuWrapper}>
Expand Down Expand Up @@ -106,7 +109,7 @@ function ActionsMenuComponent(props: Props): JSX.Element {
</Menu.Item>
<Menu.Item key={Actions.EXPORT_TASK} disabled={exportIsActive}>
{exportIsActive && <LoadingOutlined id='cvat-export-task-loading' />}
Export task
Backup task
</Menu.Item>
<Menu.Divider />
<Menu.Item key={Actions.MOVE_TASK_TO_PROJECT}>Move to project</Menu.Item>
Expand Down
15 changes: 13 additions & 2 deletions cvat-ui/src/components/projects-page/actions-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
// SPDX-License-Identifier: MIT

import React from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Modal from 'antd/lib/modal';
import Menu from 'antd/lib/menu';
import { LoadingOutlined } from '@ant-design/icons';

import { deleteProjectAsync } from 'actions/projects-actions';
import { CombinedState } from 'reducers/interfaces';
import { deleteProjectAsync, exportProjectAsync } from 'actions/projects-actions';
import { exportActions } from 'actions/export-actions';

interface Props {
Expand All @@ -18,6 +20,8 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element {
const { projectInstance } = props;

const dispatch = useDispatch();
const activeBackups = useSelector((state: CombinedState) => state.projects.activities.backups);
const exportIsActive = projectInstance.id in activeBackups;

const onDeleteProject = (): void => {
Modal.confirm({
Expand All @@ -43,6 +47,13 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element {
>
Export project dataset
</Menu.Item>
<Menu.Item
disabled={exportIsActive}
onClick={() => dispatch(exportProjectAsync(projectInstance))}
>
{exportIsActive && <LoadingOutlined id='cvat-export-project-loading' />}
Backup Project
</Menu.Item>
<hr />
<Menu.Item
key='project-delete'
Expand Down
2 changes: 1 addition & 1 deletion cvat-ui/src/components/projects-page/project-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface Props {

const useCardHeight = useCardHeightHOC({
containerClassName: 'cvat-projects-page',
siblingClassNames: ['cvat-projects-pagination', 'cvat-projects-top-bar'],
siblingClassNames: ['cvat-projects-pagination', 'cvat-projects-page-top-bar'],
paddings: 40,
numberOfRows: 3,
});
Expand Down
Loading