From d1b4c99be7dd81156975ca87c844893033b1a2e6 Mon Sep 17 00:00:00 2001
From: Stefano Alberto Russo <stefano.russo@gmail.com>
Date: Sun, 10 Apr 2022 16:01:44 +0200
Subject: [PATCH] Added support for local storages.

---
 services/webapp/code/rosetta/core_app/api.py | 555 +++++++++++--------
 1 file changed, 328 insertions(+), 227 deletions(-)

diff --git a/services/webapp/code/rosetta/core_app/api.py b/services/webapp/code/rosetta/core_app/api.py
index af006ff..3abd157 100644
--- a/services/webapp/code/rosetta/core_app/api.py
+++ b/services/webapp/code/rosetta/core_app/api.py
@@ -382,10 +382,10 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
     authentication_classes = (CsrfExemptSessionAuthentication, BasicAuthentication)
 
 
-    def scp_command(self, source, dest, user, computing, mode='get'):
+    def prepare_scp_command(self, source, dest, user, computing, mode='get'):
 
         # Prepare paths for scp. They have been already made shell-ready, but we need to triple-escape
-        # spaces on remote source or destination: My\ Folder mut become My\\\ Folder.
+        # spaces on remote source or destination: My\ Folder must become My\\\ Folder.
         
         if mode=='get':
             source = source.replace('\ ', '\\\\\\ ')
@@ -406,14 +406,22 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
         return command
 
 
-    def ssh_command(self, command, user, computing):
-
-        # Get credentials
-        computing_user, computing_host, computing_keys = get_ssh_access_mode_credentials(computing, user)
-
-        # Command
-        command = 'ssh -o LogLevel=ERROR -i {} -4 -o StrictHostKeyChecking=no {}@{} "{}"'.format(computing_keys.private_key_file, computing_user, computing_host, command)
+    def prepare_sh_command(self, command, user, storage):
 
+        if storage.access_mode == 'ssh+cli':
+            if storage.access_through_computing:
+                # Get credentials
+                computing_user, computing_host, computing_keys = get_ssh_access_mode_credentials(storage.computing, user)
+        
+                # Command
+                command = 'ssh -o LogLevel=ERROR -i {} -4 -o StrictHostKeyChecking=no {}@{} "{}"'.format(computing_keys.private_key_file, computing_user, computing_host, command)
+            else:
+                raise NotImplementedError('Not accessing through computing is not implemented for storage type "{}"'.format(storage.type))               
+        elif storage.access_mode == 'cli':
+            command = '/bin/bash -c "{}"'.format(command)
+        else:
+            raise NotImplementedError('Access mode "{}" not implemented for storage type "{}"'.format(storage.access_mode, storage.type))               
+   
         return command
 
     @staticmethod
@@ -430,11 +438,13 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
         return path
 
     @staticmethod
-    def sanitize_and_prepare_shell_path(path, storage, user):
-        path = path.replace(' ', '\ ')
-        cleaner = re.compile('(?:\\\)+')
-        path = re.sub(cleaner,r"\\",path)
-                
+    def sanitize_and_prepare_shell_path(path, user, storage, escapes=True):
+        
+        if escapes:
+            path = path.replace(' ', '\ ')
+            cleaner = re.compile('(?:\\\)+')
+            path = re.sub(cleaner,r"\\",path)
+                    
         # Prepare the base path (expand it with variables substitution)
         base_path_expanded = storage.base_path        
         if '$SSH_USER' in base_path_expanded:
@@ -505,210 +515,252 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
         # Data container 
         data = []
         
-        shell_path = self.sanitize_and_prepare_shell_path(path, storage, user)
-        
-        # Prepare command
-        # https://askubuntu.com/questions/1116634/ls-command-show-time-only-in-iso-format
-        # https://www.howtogeek.com/451022/how-to-use-the-stat-command-on-linux/
-        command = self.ssh_command('cd {} && stat --printf=\'%F/%s/%Y/%n\\n\' * .*'.format(shell_path), user, storage.computing)
-        
-        # Execute_command
-        out = os_shell(command, capture=True)
-        if out.exit_code != 0:
+        if storage.type == 'generic_posix':
+
+            shell_path = self.sanitize_and_prepare_shell_path(path, user, storage)
             
-            # Did we just get a "cannot stat - No such file or directory error?
-            if 'No such file or directory' in out.stderr:
-                
-                # Create the folder if this was the root for the user (storage base path)
-                if path == '/':
-                    self.mkdir(self.sanitize_and_prepare_shell_path('/', storage, user), user, storage, force=True)
+            # Prepare command
+            # https://askubuntu.com/questions/1116634/ls-command-show-time-only-in-iso-format
+            # https://www.howtogeek.com/451022/how-to-use-the-stat-command-on-linux/
+            
+            command = self.prepare_sh_command('cd {} && stat --printf=\'%F/%s/%Y/%n\\n\' * .*'.format(shell_path), user, storage)
+
+            # Execute_command
+            out = os_shell(command, capture=True)
+            if out.exit_code != 0:
+
+                # Did we just get a "cannot stat - No such file or directory" (bash) or a "can't cd to" (sh) error?
+                if 'No such file or directory' in out.stderr or 'can\'t cd to' in out.stderr :
+                    
+                    # Create the folder if this was the root for the user (storage base path)
+                    # Note: if the folder is completely empty, this gets execute as well.
+                    # TODO: fix me (e.g. check for "cannot stat" or similar)
+                    if path == '/':
+                        self.mkdir(self.sanitize_and_prepare_shell_path('/', user, storage), user, storage, force=True)
+                    
+                    # Return (empty) data
+                    return data
                 
-                # Return (empty) data
-                return data
+                else:
+                    raise Exception(out.stderr)
+                                
+            # Log        
+            #logger.debug('Shell exec output: "{}"'.format(out))
             
-            else:
-                raise Exception(out.stderr)
-                            
-        # Log        
-        #logger.debug('Shell exec output: "{}"'.format(out))
-        
-        out_lines = out.stdout.split('\n')
-        
-        for line in out_lines:
+            out_lines = out.stdout.split('\n')
             
-            # Example line: directory/My folder/68/1617030350
-
-            # Set name
-            line_pieces = line.split('/')
-            type = line_pieces[0]
-            size = line_pieces[1]
-            timestamp = line_pieces[2]
-            name = line_pieces[3]
-                     
-            # Define and clean listing path:
-            listing_path = '/{}/{}/{}/'.format(storage.id, path, name)
-            listing_path = self.clean_path(listing_path)
-        
-            # File or directory?
-            if type == 'directory':
-                if name not in ['.', '..']:
+            for line in out_lines:
+                
+                # Example line: directory/My folder/68/1617030350
+    
+                # Set name
+                line_pieces = line.split('/')
+                type = line_pieces[0]
+                size = line_pieces[1]
+                timestamp = line_pieces[2]
+                name = line_pieces[3]
+                         
+                # Define and clean listing path:
+                listing_path = '/{}/{}/{}/'.format(storage.id, path, name)
+                listing_path = self.clean_path(listing_path)
+            
+                # File or directory?
+                if type == 'directory':
+                    if name not in ['.', '..']:
+                        data.append({
+                                     'id': listing_path,
+                                     'type': 'folder',
+                                     'attributes':{
+                                          'modified': timestamp,
+                                          'name': name,
+                                          'readable': 1,
+                                          'writable': 1,
+                                          'path': listing_path                                 
+                                      }
+                                     })
+                else:
                     data.append({
-                                 'id': listing_path,
-                                 'type': 'folder',
+                                 'id': listing_path[:-1], # Remove trailing slash 
+                                 'type': 'file',
                                  'attributes':{
                                       'modified': timestamp,
                                       'name': name,
                                       'readable': 1,
                                       'writable': 1,
-                                      'path': listing_path                                 
+                                      "size": size,
+                                      'path': listing_path[:-1] # Remove trailing slash                               
                                   }
-                                 })
-            else:
-                data.append({
-                             'id': listing_path[:-1], # Remove trailing slash 
-                             'type': 'file',
-                             'attributes':{
-                                  'modified': timestamp,
-                                  'name': name,
-                                  'readable': 1,
-                                  'writable': 1,
-                                  "size": size,
-                                  'path': listing_path[:-1] # Remove trailing slash                               
-                              }
-                             })                            
-            
+                                 })                            
+                
+        else:
+            raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type))               
             
         return data
 
 
     def stat(self, path, user, storage):
-        
-        path = self.sanitize_and_prepare_shell_path(path, storage, user)
-        
-        # Prepare command. See the ls function above for some more info
-        command = self.ssh_command('stat --printf=\'%F/%s/%Y/%n\\n\' {}'.format(path), user, storage.computing)
-        
-        # Execute_command
-        out = os_shell(command, capture=True)
-        if out.exit_code != 0:
+
+        # Data container 
+        data = []
+
+        if storage.type == 'generic_posix':
+
+            shell_path = self.sanitize_and_prepare_shell_path(path, user, storage)
             
-            # Did we just get a "cannot stat - No such file or directory error?
-            if 'No such file or directory' in out.stderr:
-                pass
-            else:
-                raise Exception(out.stderr)
-                            
-        # Log        
-        #logger.debug('Shell exec output: "{}"'.format(out))
+            # Prepare command. See the ls function above for some more info
+            command = self.prepare_sh_command('stat --printf=\'%F/%s/%Y/%n\\n\' {}'.format(shell_path), user, storage)
+            
+            # Execute_command
+            out = os_shell(command, capture=True)
+            if out.exit_code != 0:
+                
+                # Did we just get a "cannot stat - No such file or directory error?
+                if 'No such file or directory' in out.stderr:
+                    pass
+                else:
+                    raise Exception(out.stderr)
+                                
+            # Log        
+            #logger.debug('Shell exec output: "{}"'.format(out))
+            
+            out_lines = out.stdout.split('\n')
+            if len(out_lines) > 1:
+                raise Exception('Internal error on stat: more than one ouput line')
+            out_line = out_lines[0]
         
-        out_lines = out.stdout.split('\n')
-        if len(out_lines) > 1:
-            raise Exception('Internal error on stat: more than one ouput line')
-        out_line = out_lines[0]
+            # Example output line: directory:My folder:68/1617030350
+            # In this context, we also might get the following output:
+            # directory/68/1617030350//My folder/
+            # ..so, use the clean path to remove all extra slashes.
+            # The only uncovered case is to rename the root folder...
+            
+            out_line = self.clean_path(out_line)
     
-        # Example output line: directory:My folder:68/1617030350
-        # In this context, we also might get the following output:
-        # directory/68/1617030350//My folder/
-        # ..so, use the clean path to remove all extra slashes.
-        # The only uncovered case is to rename the root folder...
-        
-        out_line = self.clean_path(out_line)
-
-        # Set names
-        line_pieces = out_line.split('/')
-        type = line_pieces[0]
-        size = line_pieces[1]
-        timestamp = line_pieces[2]
-        name = '/'.join(line_pieces[3:])
+            # Set names
+            line_pieces = out_line.split('/')
+            type = line_pieces[0]
+            size = line_pieces[1]
+            timestamp = line_pieces[2]
+            name = '/'.join(line_pieces[3:])
+            
+            data = {'type': type, 'name': name, 'size': size, 'timestamp': timestamp}
+
+        else:
+            raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type))                           
         
-        return {'type': type, 'name': name, 'size': size, 'timestamp': timestamp}
+        return data
             
 
 
     def delete(self, path, user, storage):
 
-        path = self.sanitize_and_prepare_shell_path(path, storage, user)
+        if storage.type == 'generic_posix':
 
-        # Prepare command
-        command = self.ssh_command('rm -rf {}'.format(path), user, storage.computing)
+            shell_path = self.sanitize_and_prepare_shell_path(path, user, storage)
+    
+            # Prepare command
+            command = self.prepare_sh_command('rm -rf {}'.format(shell_path), user, storage)
+            
+            # Execute_command
+            out = os_shell(command, capture=True)
+            if out.exit_code != 0:
+                raise Exception(out.stderr)
         
-        # Execute_command
-        out = os_shell(command, capture=True)
-        if out.exit_code != 0:
-            raise Exception(out.stderr)
-        return out.stdout
-
+        else:
+            raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type))               
+            
 
     def mkdir(self, path, user, storage, force=False):
+
+        path = self.sanitize_and_prepare_shell_path(path, user, storage)
+
+        if storage.type == 'generic_posix':
+            # Prepare command
+            if force:
+                command = self.prepare_sh_command('mkdir -p {}'.format(path), user, storage)
+            else:
+                command = self.prepare_sh_command('mkdir {}'.format(path), user, storage)
+            
+            # Execute_command
+            out = os_shell(command, capture=True)
+            if out.exit_code != 0:
+                raise Exception(out.stderr)
         
-        path = self.sanitize_and_prepare_shell_path(path, storage, user)
-        
-        # Prepare command
-        if force:
-            command = self.ssh_command('mkdir -p {}'.format(path), user, storage.computing)
         else:
-            command = self.ssh_command('mkdir {}'.format(path), user, storage.computing)
-        
-        # Execute_command
-        out = os_shell(command, capture=True)
-        if out.exit_code != 0:
-            raise Exception(out.stderr)
-        return out.stdout
+            raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type))               
 
 
     def cat(self, path, user, storage):
         
-        path = self.sanitize_and_prepare_shell_path(path, storage, user)
-        
-        # Prepare command
-        command = self.ssh_command('cat {}'.format(path), user, storage.computing)
-        
-        # Execute_command
-        out = os_shell(command, capture=True)
-        if out.exit_code != 0:
-            raise Exception(out.stderr)
-        return out.stdout
+        # Data container 
+        data = []
+
+        if storage.type == 'generic_posix':
+   
+            shell_path = self.sanitize_and_prepare_shell_path(path, user, storage)
+            
+            # Prepare command
+            command = self.prepare_sh_command('cat {}'.format(shell_path), user, storage)
+            
+            # Execute_command
+            out = os_shell(command, capture=True)
+            if out.exit_code != 0:
+                raise Exception(out.stderr)
+            data = out.stdout
 
+        else:
+            raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type))               
 
-    def rename(self, old, new, user, storage):
-        
-        old = self.sanitize_and_prepare_shell_path(old, storage, user)
-        new = self.sanitize_and_prepare_shell_path(new, storage, user)
+        return data
 
-        # Prepare command
-        command = self.ssh_command('mv {} {}'.format(old, new), user, storage.computing)
 
-        logger.critical(command)
+    def rename(self, old, new, user, storage):
+
+        if storage.type == 'generic_posix':
+       
+            old = self.sanitize_and_prepare_shell_path(old, user, storage)
+            new = self.sanitize_and_prepare_shell_path(new, user, storage)
+    
+            # Prepare command
+            command = self.prepare_sh_command('mv {} {}'.format(old, new), user, storage)
+    
+            logger.critical(command)
+            
+            # Execute_command
+            out = os_shell(command, capture=True)
+            if out.exit_code != 0:
+                raise Exception(out.stderr)
         
-        # Execute_command
-        out = os_shell(command, capture=True)
-        if out.exit_code != 0:
-            raise Exception(out.stderr)
-        return out.stdout
+        else:
+            raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type))               
 
 
     def copy(self, source, target, user, storage):
 
-        source = self.sanitize_and_prepare_shell_path(source, storage, user)
-        target = self.sanitize_and_prepare_shell_path(target, storage, user)
+        if storage.type == 'generic_posix':
 
-        # Prepare command
-        command = self.ssh_command('cp -a {} {}'.format(source, target), user, storage.computing)
-        
-        # Execute_command
-        out = os_shell(command, capture=True)
-        if out.exit_code != 0:
-            raise Exception(out.stderr)
-        return out.stdout
+            source = self.sanitize_and_prepare_shell_path(source, user, storage)
+            target = self.sanitize_and_prepare_shell_path(target, user, storage)
+    
+            # Prepare command
+            command = self.prepare_sh_command('cp -a {} {}'.format(source, target), user, storage)
+            
+            # Execute_command
+            out = os_shell(command, capture=True)
+            if out.exit_code != 0:
+                raise Exception(out.stderr)
+            
+        else:
+            raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type))               
 
 
     def scp_from(self, source, target, user, storage, mode='get'):
 
-        source = self.sanitize_and_prepare_shell_path(source, storage, user)
+        source = self.sanitize_and_prepare_shell_path(source, user, storage)
         target = self.sanitize_shell_path(target) # This is a folder on Rosetta (/tmp)
 
         # Prepare command
-        command = self.scp_command(source, target, user, storage.computing, mode)
+        command = self.prepare_scp_command(source, target, user, storage.computing, mode)
 
         # Execute_command
         out = os_shell(command, capture=True)
@@ -719,10 +771,10 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
     def scp_to(self, source, target, user, storage, mode='get'):
 
         source = self.sanitize_shell_path(source) # This is a folder on Rosetta (/tmp)
-        target = self.sanitize_and_prepare_shell_path(target, storage, user)
+        target = self.sanitize_and_prepare_shell_path(target, user, storage)
 
         # Prepare command
-        command = self.scp_command(source, target, user, storage.computing, mode)
+        command = self.prepare_scp_command(source, target, user, storage.computing, mode)
 
         # Execute_command
         out = os_shell(command, capture=True)
@@ -794,37 +846,60 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
 
         elif mode in ['download', 'getimage']:
             logger.debug('Downloading "{}"'.format(path))
+
+            # Set support vars
+            storage = self.get_storage_from_path(path, request)
+            file_path = '/'+'/'.join(path.split('/')[2:])
             
             if path.endswith('/'):
                 return error400('Downloading a folder is not supported')
             
-            # TOOD: here we are not handling ajax request, Maybe they have been deperacted?
+            if storage.type != 'generic_posix':
+                raise NotImplementedError('Storage type "{}" not implemented'.format(storage.type))               
+
+            # TODO: here we are not handling the ajax request, Maybe they have been deperacted?
             # The download process consists of 2 requests:
             #  - Ajax GET request. Perform all checks and validation. Should return file/folder object in the response data to proceed.
             #  - Regular GET request. Response headers should be properly configured to output contents to the browser and start download.
             # See here: https://github.com/psolom/RichFilemanager/wiki/API
 
-            # Set support vars
-            storage = self.get_storage_from_path(path, request)
-            file_path = '/'+'/'.join(path.split('/')[2:])
-            target_path = '/tmp/{}'.format(uuid.uuid4())
-
-            # Get the file
-            self.scp_from(file_path, target_path, request.user, storage, mode='get') 
+            if storage.access_mode == 'ssh+cli':
+                target_path = '/tmp/{}'.format(uuid.uuid4())
+    
+                # Get the file
+                self.scp_from(file_path, target_path, request.user, storage, mode='get') 
+    
+                # Detect content type
+                try:
+                    content_type =  str(magic.from_file(target_path, mime=True))
+                except:
+                    content_type = None
+    
+                # Read file data
+                with open(target_path, 'rb') as f:
+                    data = f.read()
+                
+                # Remove temporary file
+                os.remove(target_path)
+                
+            elif storage.access_mode == 'cli':
 
-            # Detect content type
-            try:
-                content_type =  str(magic.from_file(target_path, mime=True))
-            except:
-                content_type = None
+                # Define storage internal source path
+                storage_source_path = self.sanitize_and_prepare_shell_path(file_path, request.user, storage, escapes=False)
 
-            # Read file data
-            with open(target_path, 'rb') as f:
-                data = f.read()
-            
-            # Remove file
-            os.remove(target_path)
+                # Detect content type
+                try:
+                    content_type =  str(magic.from_file(storage_source_path, mime=True))
+                except:
+                    content_type = None
+    
+                # Read file data
+                with open(storage_source_path, 'rb') as f:
+                    data = f.read()
             
+            else:
+                raise NotImplementedError('Storage access mode "{}" not implemented'.format(storage.access_mode))               
+
             # Return file data
             response = HttpResponse(data, status=status.HTTP_200_OK, content_type=content_type)
             response['Content-Disposition'] = 'attachment; filename="{}"'.format(file_path.split('/')[-1])
@@ -857,8 +932,8 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
             else:
                 is_folder=False
 
-            # Get file contents
-            data = self.delete(path, request.user, storage)
+            # Delete
+            self.delete(path, request.user, storage)
 
             # Response data
             data = { 'data': {
@@ -889,8 +964,8 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
             storage = self.get_storage_from_path(path, request)
             path = '/'+'/'.join(path.split('/')[2:]) + name
 
-            # Get file contents
-            data = self.mkdir(path, request.user, storage)
+            # Create the directory
+            self.mkdir(path, request.user, storage)
 
             # Response data
             data = { 'data': {
@@ -906,7 +981,6 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
                         }
                     }      
             
-            
             # Return file contents
             return Response(data, status=status.HTTP_200_OK)
 
@@ -1038,12 +1112,10 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
             
             # Return file contents
             return Response(data, status=status.HTTP_200_OK)
-
-        
+  
         else:
             return error400('Operation "{}" not supported'.format(mode))
-
-        
+  
         return Response(data, status=status.HTTP_200_OK)
 
 
@@ -1057,7 +1129,6 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
         path = request.POST.get('path', None)
         _ = request.GET.get('_', None)
 
-
         if mode == 'savefile':
             return error400('Operation "{}" not supported'.format(mode))
         
@@ -1073,44 +1144,74 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI):
 
             # Get the file upload
             file_upload = request.FILES['files']
-            
-            # generate temporary UUID
-            file_uuid = uuid.uuid4()
-            
-            with open('/tmp/{}'.format(file_uuid), 'wb') as temp_file:
-                temp_file.write(file_upload.read())
-            
-            logger.debug('Wrote "/tmp/{}" for "{}"'.format(file_uuid, file_upload.name))
 
-            # Now copy with scp
-            self.scp_to('/tmp/{}'.format(file_uuid), path + file_upload.name , request.user, storage, mode='put')
-        
-            # Response data
-            data = { 'data': [{
-                            'id': '/{}{}{}'.format(storage.id, path, file_upload.name),
-                            'type': 'file',
-                            'attributes':{
-                                'modified': now_t(),  # This is an approximation!
-                                'name': file_upload.name,
-                                'readable': 1,
-                                'size': os.path.getsize('/tmp/{}'.format(file_uuid)), # This is kind of an approximation!
-                                'writable': 1,
-                                'path': '/{}{}{}'.format(storage.id, path, file_upload.name)                            
-                            }
-                        }]
-                    }
+            if storage.access_mode == 'ssh+cli':
+                
+                # Generate temporary UUID
+                file_uuid = uuid.uuid4()
+                
+                with open('/tmp/{}'.format(file_uuid), 'wb') as temp_file:
+                    temp_file.write(file_upload.read())
+                
+                logger.debug('Wrote "/tmp/{}" for "{}"'.format(file_uuid, file_upload.name))
+    
+                # Now copy with scp
+                self.scp_to('/tmp/{}'.format(file_uuid), path + file_upload.name , request.user, storage, mode='put')
             
-            # Remove file
-            os.remove('/tmp/{}'.format(file_uuid))
-                        
+                # Response data
+                data = { 'data': [{
+                                'id': '/{}{}{}'.format(storage.id, path, file_upload.name),
+                                'type': 'file',
+                                'attributes':{
+                                    'modified': now_t(),  # This is an approximation!
+                                    'name': file_upload.name,
+                                    'readable': 1,
+                                    'size': os.path.getsize('/tmp/{}'.format(file_uuid)), # This is kind of an approximation!
+                                    'writable': 1,
+                                    'path': '/{}{}{}'.format(storage.id, path, file_upload.name)                            
+                                }
+                            }]
+                        }
+                
+                # Remove file
+                os.remove('/tmp/{}'.format(file_uuid)) 
+                   
+            if storage.access_mode == 'cli':
+          
+                # Define storage internal dest path
+                storage_dest_path = self.sanitize_and_prepare_shell_path(path + file_upload.name, request.user, storage, escapes=False)
+
+                logger.debug('Writing "{}" for "{}"'.format(storage_dest_path, file_upload.name))
+               
+                with open(storage_dest_path, 'wb') as upload_file:
+                    upload_file.write(file_upload.read())
+                
+                logger.debug('Wrote "{}" for "{}"'.format(storage_dest_path, file_upload.name))
+    
+                # Response data
+                data = { 'data': [{
+                                'id': '/{}{}{}'.format(storage.id, path, file_upload.name),
+                                'type': 'file',
+                                'attributes':{
+                                    'modified': now_t(),  # This is an approximation!
+                                    'name': file_upload.name,
+                                    'readable': 1,
+                                    'size': os.path.getsize(storage_dest_path),
+                                    'writable': 1,
+                                    'path': '/{}{}{}'.format(storage.id, path, file_upload.name)                            
+                                }
+                            }]
+                        }
+
+            else:
+                raise NotImplementedError('Storage access mode "{}" not implemented'.format(storage.access_mode))               
+              
             # Return
             return Response(data, status=status.HTTP_200_OK)
         
         else:
             return error400('Operation "{}" not supported'.format(mode))
 
-        return ok200('ok')
-
 
 #==============================
 #  Import repository APIs
-- 
GitLab