Skip to content

Commit

Permalink
create a copy of a locked file
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrWichlinskiSoftwaremind authored and diocas committed May 15, 2023
1 parent 51b0c3f commit e291dbe
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/CI/revad-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: "3.9"
services:
revad:
image: cs3org/revad:v1.21.0 # 'latest' tag is temporarily not available
image: cs3org/revad # 'latest' tag is temporarily not available
container_name: revad
ports:
- "19000-19001:19000-19001"
Expand Down
51 changes: 50 additions & 1 deletion cs3api4lab/api/cs3apismanager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import urllib
import nbformat
import os
import posixpath
Expand Down Expand Up @@ -349,6 +350,12 @@ def _file_model(self, path, content, format):
model = ModelUtils.create_empty_file_model(path)
try:
file_info = self.file_api.stat_info(path, self.cs3_config.endpoint)

# additional request until this issue is resolved https://github.com/cs3org/reva/issues/3243
if self.config.dev_env and "/home/" in file_info['filepath']:
opaque_id = urllib.parse.unquote(file_info['inode']['opaque_id'])
storage_id = urllib.parse.unquote(file_info['inode']['storage_id'])
file_info = self.file_api.stat_info(opaque_id, storage_id)
except Exception as e:
self.log.info('File % does not exists' % path)

Expand Down Expand Up @@ -379,9 +386,15 @@ def _file_model(self, path, content, format):
def _notebook_model(self, path, content):
file_info = self.file_api.stat_info(path, self.cs3_config.endpoint)

# additional request until this issue is resolved https://github.com/cs3org/reva/issues/3243
if self.config.dev_env and "/home/" in file_info['filepath']:
opaque_id = urllib.parse.unquote(file_info['inode']['opaque_id'])
storage_id = urllib.parse.unquote(file_info['inode']['storage_id'])
file_info = self.file_api.stat_info(opaque_id, storage_id)

model = ModelUtils.update_file_model(ModelUtils.create_empty_file_model(path), file_info)
model['type'] = 'notebook'

model['locked'] = self.lock_api.is_file_locked(file_info)
if content:
file_content = self._read_file(file_info)
nb = nbformat.reads(file_content, as_version=4)
Expand All @@ -390,6 +403,8 @@ def _notebook_model(self, path, content):
model['format'] = 'json'
self.validate_notebook_model(model)

model['writable'] = self._is_editor(file_info)

return model

@asyncify
Expand Down Expand Up @@ -495,3 +510,37 @@ def list_checkpoints(self, path):

def delete_checkpoint(self, checkpoint_id, path):
pass

@asyncify
def create_clone_file(self, path):
path_normalized = FileUtils.normalize_path(path)
path_normalized = FileUtils.check_and_transform_file_path(path_normalized)
notebook_container = self.cs3_config.home_dir if self.cs3_config.home_dir else self.cs3_config.mount_dir

file_info = self.file_api.stat_info(path_normalized, self.config.endpoint)
# additional request until this issue is resolved https://github.com/cs3org/reva/issues/3243
if self.config.dev_env and "/home/" in file_info['filepath']:
opaque_id = urllib.parse.unquote(file_info['inode']['opaque_id'])
storage_id = urllib.parse.unquote(file_info['inode']['storage_id'])
file_info = self.file_api.stat_info(opaque_id, storage_id)

clone_file = self.lock_api.resolve_file_path(path)
clone_file_exists = self.file_exists(clone_file)
clone_file_created = False
clone_file_path = ""
if not clone_file_exists:
try:
file_content = self._read_file(file_info)
clone_file_path = FileUtils.normalize_path(notebook_container + '/' + clone_file)
clone_file_path = FileUtils.check_and_transform_file_path(clone_file_path)

self.file_api.write_file(clone_file_path, file_content, self.cs3_config.endpoint, None)

clone_file_created = True
except Exception:
self.log.info('Could not create a clone file from original %s', path)

return {
'conflict_file_path': clone_file_path,
'conflict_file_created': clone_file_created
}
15 changes: 14 additions & 1 deletion cs3api4lab/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ def file_api(self):
def get(self):
yield RequestHandler.async_handle_request(self, self.file_api.get_home_dir, 200)

class LockHandler(APIHandler):

@property
def contents_manager(self):
return self.settings["contents_manager"]

@web.authenticated
@gen.coroutine
def post(self):
request = self.get_json_body()
yield RequestHandler.async_handle_request(self, self.contents_manager.create_clone_file, 200, request['file_path'])

class PublicSharesHandler(APIHandler):
@property
def public_share_api(self):
Expand Down Expand Up @@ -219,7 +231,8 @@ def setup_handlers(web_app, url_path):
(r"/api/cs3/user", UserInfoHandler),
(r"/api/cs3/user/claim", UserInfoClaimHandler),
(r"/api/cs3/user/query", UserQueryHandler),
(r"/api/cs3/user/home_dir", HomeDirHandler)
(r"/api/cs3/user/home_dir", HomeDirHandler),
(r"/api/cs3/locks/create_clone_file", LockHandler),
]

for handler in handlers:
Expand Down
12 changes: 4 additions & 8 deletions cs3api4lab/locks/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,15 @@ def get_current_user(self):
metadata=[('x-access-token', self.auth.authenticate())])
return self.user.user

def resolve_file_path(self, stat):
if self.is_valid_external_lock(stat):
file_name = stat['filepath'].split('/')[-1]
file_dir = '/'.join(stat['filepath'].split('/')[0:-1])
return self._resolve_directory(file_dir, self.config.endpoint) + self._get_conflict_filename(file_name)
return stat['filepath']
def resolve_file_path(self, path):
file_name = path.split('/')[-1]
return self._get_conflict_filename(file_name)

@abstractmethod
def is_valid_external_lock(self, stat):
pass

def _resolve_directory(self, dir_path,
endpoint): # right now it's possible to write in somone else's directory without it being shared
def _resolve_directory(self, dir_path, endpoint): # right now it's possible to write in somone else's directory without it being shared
stat = self.storage_api.stat(dir_path, endpoint)
if stat.status.code == cs3code.CODE_OK:
return dir_path
Expand Down
14 changes: 7 additions & 7 deletions cs3api4lab/tests/test_cs3apismanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_get_text_file(self):
self.file_api.write_file(file_id, message, self.endpoint)
model = self.contents_manager.get(file_id, True, 'file')
self.assertEqual(model["name"], "test_get_text_file.txt")
self.assertEqual(model["path"], file_id)
self.assertEqual(model["path"], "/reva/einstein/test_get_text_file.txt")
self.assertEqual(model["content"], message)
self.assertEqual(model["format"], "text")
self.assertEqual(model["mimetype"], "text/plain")
Expand All @@ -49,7 +49,7 @@ def test_get_text_file_without_type(self):
self.file_api.write_file(file_id, message, self.endpoint)
model = self.contents_manager.get(file_id, True, None)
self.assertEqual(model["name"], "test_get_text_file_no_type.txt")
self.assertEqual(model["path"], file_id)
self.assertEqual(model["path"], "/reva/einstein/test_get_text_file_no_type.txt")
self.assertEqual(model["content"], message)
self.assertEqual(model["format"], "text")
self.assertEqual(model["mimetype"], "text/plain")
Expand Down Expand Up @@ -116,7 +116,7 @@ def test_get_notebook_file(self):
self.file_api.write_file(file_id, buffer, self.endpoint)
model = self.contents_manager.get(file_id, True, "notebook")
self.assertEqual(model["name"], "test_get_notebook_file.ipynb")
self.assertEqual(model["path"], file_id)
self.assertEqual(model["path"], "/reva/einstein/test_get_notebook_file.ipynb")
self.assertIn("### Markdown example", str(model["content"]))
self.assertEqual(model["format"], "json")
self.assertEqual(model["mimetype"], None)
Expand All @@ -136,7 +136,7 @@ def test_save_text_model(self):
try:
save_model = self.contents_manager.save(model, file_id)
self.assertEqual(save_model["name"], "test_save_text_model.txt")
self.assertEqual(save_model["path"], file_id)
self.assertEqual(save_model["path"], "/reva/einstein/test_save_text_model.txt")
self.assertEqual(save_model["content"], None)
self.assertEqual(save_model["format"], None)
self.assertEqual(save_model["mimetype"], "text/plain")
Expand All @@ -152,7 +152,7 @@ def test_save_notebook_model(self):
try:
save_model = self.contents_manager.save(model, file_id)
self.assertEqual(save_model["name"], "test_save_notebook_model.ipynb")
self.assertEqual(save_model["path"], file_id)
self.assertEqual(save_model["path"], "/reva/einstein/test_save_notebook_model.ipynb")
self.assertEqual(save_model["content"], None)
self.assertEqual(save_model["format"], None)
self.assertEqual(save_model["mimetype"], None)
Expand Down Expand Up @@ -315,7 +315,7 @@ def test_new_file_model(self):
self.contents_manager.new(model, file_path)
model = self.contents_manager.get(file_path, True, 'file')
self.assertEqual(model["name"], "test_new_file_model.txt")
self.assertEqual(model["path"], file_path)
self.assertEqual(model["path"], "/reva/einstein/test_new_file_model.txt")
self.assertEqual(model["content"], "Test content")
self.assertEqual(model["format"], "text")
self.assertEqual(model["mimetype"], "text/plain")
Expand All @@ -331,7 +331,7 @@ def test_new_notebook_model(self):
try:
save_model = self.contents_manager.new(model, file_path)
self.assertEqual(save_model["name"], "test_new_notebook_model.ipynb")
self.assertEqual(save_model["path"], file_path)
self.assertEqual(save_model["path"], "/reva/einstein/test_new_notebook_model.ipynb")
self.assertEqual(save_model["content"], None)
self.assertEqual(save_model["format"], None)
self.assertEqual(save_model["mimetype"], None)
Expand Down
14 changes: 4 additions & 10 deletions cs3api4lab/utils/share_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,10 @@ def get_resource_permissions(role):
def map_permissions_to_role(permissions):
if permissions is None:
return None
if permissions.get_path is True and \
permissions.initiate_file_download is True and \
permissions.list_container is True and \
permissions.stat is True and \
permissions.create_container is True and \
permissions.delete is True and \
permissions.initiate_file_upload is True and \
permissions.restore_file_version is True and \
permissions.move is True:
return Role.EDITOR

if permissions.initiate_file_upload is True and \
permissions.restore_file_version is True:
return Role.EDITOR
else:
return Role.VIEWER

Expand Down
95 changes: 80 additions & 15 deletions src/drive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { DocumentRegistry } from '@jupyterlab/docregistry';
import { ISignal, Signal } from '@lumino/signaling';
import { IStateDB } from '@jupyterlab/statedb';
import { IDocumentManager } from '@jupyterlab/docmanager';
import { Dialog, showDialog } from '@jupyterlab/apputils';
import { ConflictFileResponse } from './types';

export class CS3Contents implements Contents.IDrive {
protected _docRegistry: DocumentRegistry;
Expand Down Expand Up @@ -55,7 +57,7 @@ export class CS3Contents implements Contents.IDrive {
* The name of the drive.
*/
get name(): string {
return '';
return 'cs3Files';
}

/**
Expand All @@ -73,7 +75,13 @@ export class CS3Contents implements Contents.IDrive {
): Promise<Contents.IModel> {
const activeTab: string = (await this._state.fetch('activeTab')) as string;
if (activeTab === 'cs3filebrowser' || activeTab === undefined) {
return await CS3ContainerFiles('filelist', this._state, path, options);
return await CS3ContainerFiles(
'filelist',
this._state,
path,
options,
this._docManager
);
} else {
return Promise.resolve({} as Contents.IModel);
}
Expand Down Expand Up @@ -170,7 +178,13 @@ export class CS3ContentsShareByMe extends CS3Contents {
): Promise<Contents.IModel> {
const activeTab: string = (await this._state.fetch('activeTab')) as string;
if (activeTab === 'sharesPanel') {
return await CS3ContainerFiles('by_me', this._state, path, options);
return await CS3ContainerFiles(
'by_me',
this._state,
path,
options,
this._docManager
);
} else {
return Promise.resolve({} as Contents.IModel);
}
Expand All @@ -188,7 +202,13 @@ export class CS3ContentsShareWithMe extends CS3Contents {
): Promise<Contents.IModel> {
const activeTab: string = (await this._state.fetch('activeTab')) as string;
if (activeTab === 'sharesPanel') {
return await CS3ContainerFiles('with_me', this._state, path, options);
return await CS3ContainerFiles(
'with_me',
this._state,
path,
options,
this._docManager
);
} else {
return Promise.resolve({} as Contents.IModel);
}
Expand All @@ -199,7 +219,8 @@ export async function CS3ContainerFiles(
readType: string,
stateDB: IStateDB,
path: string | null = null,
options: Contents.IFetchOptions = {}
options: Contents.IFetchOptions = {},
docManager: IDocumentManager
): Promise<any> {
const share = await stateDB.fetch('share');
const showHidden: boolean = (await stateDB.fetch('showHidden')) as boolean;
Expand All @@ -211,7 +232,7 @@ export async function CS3ContainerFiles(
}

if (path !== '') {
return await getFileList(path, options, showHidden, stateDB);
return await getFileList(path, options, showHidden, stateDB, docManager);
}

switch (shareType) {
Expand All @@ -221,15 +242,55 @@ export async function CS3ContainerFiles(
return await getSharedWithMe();
case 'filelist':
default:
return await getFileList(path, options, showHidden, stateDB);
return await getFileList(path, options, showHidden, stateDB, docManager);
}
}
export function openLockedFileDialog(
path: string,
docManager: IDocumentManager
): void {
showDialog({
body: 'This file is currently locked by another person',
buttons: [
Dialog.cancelButton({
label: 'Stay in preview mode',
className: 'jp-preview-button'
}),
Dialog.okButton({
label: 'Create a copy',
className: 'jp-create-button'
})
]
}).then(async result => {
if (result.button.className === 'jp-create-button') {
await requestAPI('/api/cs3/locks/create_clone_file', {
method: 'POST',
body: JSON.stringify({
file_path: path
})
})
.then(async response => {
const diffResponse = response as ConflictFileResponse;
if (diffResponse.conflict_file_path) {
docManager.openOrReveal(diffResponse.conflict_file_path);
await docManager.closeFile('cs3Files:' + path);
await docManager.closeFile('cs3driveShareWithMe:' + path);
await docManager.closeFile('cs3driveShareByMe:' + path);
}
})
.catch(error => {
console.log('request failed', error);
});
}
});
}

async function getFileList(
path: string | null,
options: Contents.IFetchOptions,
showHidden: boolean,
stateDB: IStateDB
stateDB: IStateDB,
docManager: IDocumentManager
): Promise<any> {
const { type, format, content } = options;

Expand All @@ -241,16 +302,20 @@ async function getFileList(
if (format && type !== 'notebook') {
url += '&format=' + format;
}
const result: Contents.IModel = await requestAPI(
'/api/contents/' + path + '' + url,
{ method: 'get' }
);
const result: Contents.IModel & {
locked: boolean;
} = await requestAPI('/api/contents/' + path + '' + url, { method: 'get' });

if (path !== null && result.type !== 'directory' && result?.locked) {
openLockedFileDialog(path, docManager);
}

// if it is a directory, count hidden files inside
if (Array.isArray(result.content) && result.type === 'directory') {
const hiddenFilesNo: number = result.content.filter(
(file: { name: string }) => file.name.startsWith('.')
).length;
const hiddenFilesNo: number =
result.content.filter((file: { name: string }) =>
file.name.startsWith('.')
)?.length || 0;
await stateDB.save('hiddenFilesNo', hiddenFilesNo);

if (!showHidden) {
Expand Down
Loading

0 comments on commit e291dbe

Please sign in to comment.