Repository: TACC/agavepy

Issue:
Implement a method to copy files within remote systems
Implement a method that allows the user to copy a file or directory from one remote system to another or within the same remote system.

Discussion:

Patch:
diff --git a/agavepy/agave.py b/agavepy/agave.py
index 53dfcb1..4eb104e 100644
--- a/agavepy/agave.py
+++ b/agavepy/agave.py
@@ -23,7 +23,8 @@ from agavepy.tenants import tenant_list
 from agavepy.clients import clients_create, clients_list
 from agavepy.tokens import token_create, refresh_token
 from agavepy.utils import load_config, save_config
-from agavepy.files import files_delete, files_download, files_list, files_upload
+from agavepy.files import (files_copy, files_delete, files_download, 
+    files_list, files_upload)
 
 
 import sys
@@ -727,6 +728,16 @@ class Agave(object):
             self.expires_at    = token_data["expires_at"]
 
 
+    def files_copy(self, source, destination):
+        """ Copy a file from source to destination on a remote system
+        """
+        # Check if tokens need to be refreshed.
+        self.refresh_tokens()
+
+        # Make a copy of the file.
+        files_copy(self.api_server, self.token, source, destination)
+
+
     def files_delete(self, file_path):
         """ Delete a file from remote system
         """
diff --git a/agavepy/files/__init__.py b/agavepy/files/__init__.py
index 6b72224..b7043b2 100644
--- a/agavepy/files/__init__.py
+++ b/agavepy/files/__init__.py
@@ -1,3 +1,4 @@
+from .copy import files_copy
 from .delete import files_delete
 from .download import files_download
 from .list import files_list
diff --git a/agavepy/files/copy.py b/agavepy/files/copy.py
new file mode 100644
index 0000000..af1475b
--- /dev/null
+++ b/agavepy/files/copy.py
@@ -0,0 +1,29 @@
+"""
+    copy.py
+"""
+import requests
+from .exceptions import AgaveFilesError
+from ..utils import handle_bad_response_status_code
+
+
+def files_copy(tenant_url, access_token, source, destination):
+    """ Copy files from remote to remote system
+    """
+    # Set request url.
+    endpoint = "{0}/{1}/{2}".format(tenant_url, "files/v2/media/system", source)
+
+    # Obtain file path from remote uri. "destination" should include the system
+    # name at the begining, get rid of it.
+    destination = '/'.join( destination.split('/')[1:] )
+
+    # Make request.
+    try:
+        data = {"action": "copy", "path": destination}
+        headers  = {"Authorization":"Bearer {0}".format(access_token)}
+        params   = {"pretty": "true"}
+        resp = requests.put(endpoint, headers=headers, data=data, params=params)
+    except Exception as err:
+        raise AgaveFilesError(err)
+
+    # Handle bad status code.
+    handle_bad_response_status_code(resp)
diff --git a/agavepy/files/list.py b/agavepy/files/list.py
index 157db67..d922088 100644
--- a/agavepy/files/list.py
+++ b/agavepy/files/list.py
@@ -1,5 +1,5 @@
 """
-    files.py
+    list.py
 """
 from __future__ import print_function, division
 import py
diff --git a/agavepy/files/upload.py b/agavepy/files/upload.py
index f4cbbdd..9369e57 100644
--- a/agavepy/files/upload.py
+++ b/agavepy/files/upload.py
@@ -1,5 +1,5 @@
 """
-    download.py
+    upload.py
 """
 from __future__ import print_function
 import ntpath
diff --git a/docs/docsite/files/files.rst b/docs/docsite/files/files.rst
index 09e56e0..606a15c 100644
--- a/docs/docsite/files/files.rst
+++ b/docs/docsite/files/files.rst
@@ -72,7 +72,21 @@ the name ``cool_data.bin``.
 
 .. code-block:: pycon
 
-    >>> agave.files_upload("./important_data.ext", "tacc-globalfs-username/cool_data.bin")
+    >>> agave.files_upload("./important_data.ext",
+            "tacc-globalfs-username/cool_data.bin")
+
+
+Make a copy of a file on a remote system
+########################################
+
+So now, you have a file called ``important_data.ext`` on your remote storage
+system ``tacc-globalfs-username``. Let's make a copy of it:
+
+
+.. code-block:: pycon
+
+    >>> agave.files_copy("tacc-globalfs-username/important_data.ext", 
+            "tacc-globalfs-username/important_data-copy.ext")
 
 
 Delete a file


Test patch:
diff --git a/tests/files_test.py b/tests/files_test.py
index fe7ccdc..58168cf 100644
--- a/tests/files_test.py
+++ b/tests/files_test.py
@@ -122,6 +122,44 @@ class MockServerFilesEndpoints(BaseHTTPRequestHandler):
             self.wfile.write(json.dumps(sample_files_upload_response).encode())
 
 
+    def do_PUT(self):
+        """ Mock endpoint to test files_copy method.
+        """
+        # elements is a list of path elements, i.e., ["a", "b"] ~ "/a/b".
+        elements = self.send_headers()
+        if elements is None or not "/files/v2/media/system" in self.path:
+            self.send_response(400)
+            self.end_headers()
+            return
+
+        # Submitted form data.
+        form = cgi.FieldStorage(
+            fp = self.rfile,
+            headers = self.headers,
+            environ = {
+                "REQUEST_METHOD": "POST",
+                "CONTENT_TYPE": self.headers["Content-Type"]
+            })
+
+        # Check access token is not empty.
+        token = self.headers.getheader("Authorization")
+        if token is None or token == "":
+            self.send_response(400)
+            self.end_headers()
+            return
+
+        # Check request data.
+        if form.getvalue("action", "") == "":
+            self.send_response(400)
+            self.end_headers()
+            return
+
+        if form.getvalue("path", "") == "":
+            self.send_response(400)
+            self.end_headers()
+            return
+
+
     def do_DELETE(self):                                                          
         """ Delete file                                                         
         """                                                                     
@@ -256,3 +294,15 @@ class TestMockServer(MockServer):
             # rm dummy file in current working directory.
             if os.path.exists(tmp_file):
                 os.remove(tmp_file)
+
+    def test_files_copy(self):
+        """ test files copying from remote to remote
+
+        The call to files_copy has no side effects on the host so the function
+        call should simply be able to return successfully.
+        """
+        local_uri = "http://localhost:{port}/".format(port=self.mock_server_port)
+        agave = Agave(api_server=local_uri)
+        agave.token = "mock-access-token"
+        
+        agave.files_copy("tacc-globalfs/file", "tacc-globalfs/file-copy")