Repository: TACC/agavepy

Issue:
Write a cleaner implementation of files.download
The current implementation of `Agave.files.download` is untested and relies on the swaggerpy library that is packaged with agavepy. Write a cleaner implementation of a method to download files from a remote agave execution system.

Discussion:

Patch:
diff --git a/agavepy/agave.py b/agavepy/agave.py
index a01a71b..0acbf6b 100644
--- a/agavepy/agave.py
+++ b/agavepy/agave.py
@@ -23,6 +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_download
+
 
 import sys
 sys.path.insert(0, os.path.dirname(__file__))
@@ -700,6 +702,9 @@ class Agave(object):
 
     def refresh_tokens(self):
         """ Refresh oauth token
+
+        Check if tokens need to be created or refreshed. If tokens need to be
+        refreshed then this method will do so. Otherwise, the method will exit.
         """
         try:
             created_t = int(self.created_at)
@@ -715,13 +720,22 @@ class Agave(object):
             token_data = refresh_token(
                     self.api_key, self.api_secret, self.refresh_token, self.api_server)
 
-            self.token  = token_data["access_token"]
+            self.token         = token_data["access_token"]
             self.refresh_token = token_data["refresh_token"]
             self.expires_in    = token_data["expires_in"]
             self.created_at    = token_data["created_at"]
             self.expires_at    = token_data["expires_at"]
 
 
+    def files_download(self, source, destination):
+        """ Download files from remote system
+        """
+        # Check if tokens need to be refreshed.
+        self.refresh_tokens()
+
+        # Download file.
+        files_download(self.api_server, self.token, source, destination)
+
 
 
 
diff --git a/agavepy/files/__init__.py b/agavepy/files/__init__.py
new file mode 100644
index 0000000..f72d297
--- /dev/null
+++ b/agavepy/files/__init__.py
@@ -0,0 +1,1 @@
+from .download import files_download
diff --git a/agavepy/files/download.py b/agavepy/files/download.py
new file mode 100644
index 0000000..a1a27ad
--- /dev/null
+++ b/agavepy/files/download.py
@@ -0,0 +1,35 @@
+"""
+    copy.py
+"""
+from __future__ import print_function                                           
+import requests                                                                 
+from .exceptions import AgaveFilesError
+from ..utils import handle_bad_response_status_code
+
+
+
+def files_download(tenant_url, access_token, source, destination):
+    """ Copy a file from remote system to host
+
+    PARAMETERS
+    ----------
+    """
+    # Set request endpoint. 
+    endpoint = "{0}/{1}/{2}".format(tenant_url, "files/v2/media/system", source)
+
+    # Make request.
+    try:
+        headers = {"Authorization":"Bearer {0}".format(access_token)}
+        params  = {"pretty": "true"}
+
+        resp = requests.get(endpoint, headers=headers, params=params, stream=True)
+        with open(destination, "wb") as f:
+            for chunk in resp.iter_content(chunk_size=1024):
+                if chunk:
+                    f.write(chunk)
+    except Exception as err:
+        raise AgaveFilesError(err)
+        
+
+    # Handle bad status code.                                               
+    handle_bad_response_status_code(resp)
diff --git a/agavepy/files/exceptions.py b/agavepy/files/exceptions.py
new file mode 100644
index 0000000..feb354c
--- /dev/null
+++ b/agavepy/files/exceptions.py
@@ -0,0 +1,13 @@
+"""
+    exceptions.py
+"""
+
+
+class AgaveFilesError(Exception):
+    """ Handle files-related operation errors
+    """
+    def __init__(self, msg):
+        self.msg = msg
+    
+    def __str__(self):
+        return repr(self.msg)
diff --git a/docs/docsite/authentication/index.rst b/docs/docsite/authentication/index.rst
index 52b65b2..1e60e4f 100644
--- a/docs/docsite/authentication/index.rst
+++ b/docs/docsite/authentication/index.rst
@@ -1,3 +1,5 @@
+.. _authentication:
+
 ==============                                          
 Authentication
 ==============
diff --git a/docs/docsite/contributing_guide/contributing.rst b/docs/docsite/contributing_guide/contributing.rst
index 9d11bfd..8b72ae6 100644
--- a/docs/docsite/contributing_guide/contributing.rst
+++ b/docs/docsite/contributing_guide/contributing.rst
@@ -8,6 +8,7 @@ Contributing Guide
 
 
 .. _reporting_bugs_and_asking_for_features:
+
 Reporting bugs and asking for features
 ``````````````````````````````````````
 
@@ -18,6 +19,7 @@ functionality you believe should be implemented please file an
 
 
 .. _getting_involved:
+
 Getting involved
 ````````````````
 
diff --git a/docs/docsite/files/files-listing.rst b/docs/docsite/files/files-listing.rst
new file mode 100644
index 0000000..3b4b77c
--- /dev/null
+++ b/docs/docsite/files/files-listing.rst
@@ -0,0 +1,731 @@
+*****************
+Methods available
+*****************
+
+Summary: Move and manage data
+
+deleteFromDefaultSystem: Deletes a file or folder.
+==================================================
+``agavepy.files.deleteFromDefaultSystem(sourcefilePath)``
+
+Parameters:
+-----------
+    * **sourcefilePath**: The path of the file relative to the user's default storage location. (string)
+
+
+Response:
+---------
+    * *String*
+
+downloadFromDefaultSystem: Download a file from the user's default storage location.
+====================================================================================
+``agavepy.files.downloadFromDefaultSystem(sourcefilePath)``
+
+Parameters:
+-----------
+    * **sourcefilePath**: The path of the file relative to the user's default storage location. (string)
+
+
+Response:
+---------
+    * *None*
+
+importToDefaultSystem: Import a file via direct upload or importing from a url to the user's default storage location.
+======================================================================================================================
+``agavepy.files.importToDefaultSystem(sourcefilePath, callbackURL=None, fileName=None, fileToUpload=None, fileType=None, urlToIngest=None)``
+
+Parameters:
+-----------
+    * **sourcefilePath**: The path of the file relative to the user's default storage location. (string)
+    * **fileType**: The file format this file is in. Defaults to raw. This will be used in file transform operations. (string)
+    * **callbackURL**: The URI to notify when the import is complete. This can be an email address or http URL. If a URL is given, a GET will be made to this address. URL templating is supported. Valid template values are: ${NAME}, ${SOURCE_FORMAT}, ${DEST_FORMAT}, ${STATUS} (string)
+    * **fileName**: The name of the file after importing. If not specified, the uploaded file name will be used. (string)
+    * **urlToIngest**: The URL to import the file from. This parameter is used if not file is uploaded with this post. (string)
+    * **fileToUpload**: The file object to import. (void)
+
+
+Response:
+---------
+    * *A single RemoteFile object*
+
+**RemoteFile schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/RemoteFile.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "format": {
+          "description": "The file type of the file.", 
+          "type": "string"
+        }, 
+        "lastModified": {
+          "description": "The date this file was last modified in ISO 8601 format.", 
+          "type": "string"
+        }, 
+        "length": {
+          "description": "The length of the file/folder.", 
+          "type": "integer"
+        }, 
+        "mimeType": {
+          "description": "The mime type of the file/folder. If unknown, it defaults to application/binary.", 
+          "type": "string"
+        }, 
+        "name": {
+          "description": "The name of the file/folder.", 
+          "type": "string"
+        }, 
+        "path": {
+          "description": "The absolute path to the file/folder.", 
+          "type": "string"
+        }, 
+        "permissions": {
+          "description": "The system permission of the invoking user on the file/folder.", 
+          "type": "string"
+        }, 
+        "system": {
+          "description": "The systemId of the system where this file lives.", 
+          "type": "string"
+        }, 
+        "type": {
+          "description": "Whether it is a file or folder.", 
+          "type": "string"
+        }
+      }, 
+      "required": [], 
+      "title": "AgavePy RemoteFile schema", 
+      "type": "object"
+    }
+
+manageOnDefaultSystem: Perform an action on a file or folder.
+=============================================================
+``agavepy.files.manageOnDefaultSystem(body, sourcefilePath)``
+
+Parameters:
+-----------
+    * **sourcefilePath**: The path of the file relative to the user's default storage location. (string)
+    * **body**: The operation to perform.  (JSON, FileOperationRequest)
+
+
+**FileOperationRequest schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/FileOperationRequest.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "action": {
+          "description": "Action to perform on the file or folder.", 
+          "enum": [
+            "mkdir", 
+            "rename", 
+            "copy", 
+            "move"
+          ], 
+          "type": "string"
+        }, 
+        "path": {
+          "description": "Destination file or folder.", 
+          "type": "string"
+        }
+      }, 
+      "required": [
+        "action"
+      ], 
+      "title": "AgavePy FileOperationRequest schema", 
+      "type": "object"
+    }
+
+Response:
+---------
+    * *String*
+
+delete: Deletes a file or folder.
+=================================
+``agavepy.files.delete(filePath, systemId)``
+
+Parameters:
+-----------
+    * **systemId**: The unique id of the system on which the data resides. (string)
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+
+
+Response:
+---------
+    * *String*
+
+download: Download a file from the user's default storage location.
+===================================================================
+``agavepy.files.download(filePath, systemId)``
+
+Parameters:
+-----------
+    * **systemId**: The unique id of the system on which the data resides. (string)
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+
+
+Response:
+---------
+    * *None*
+
+importData: Import a file via direct upload or importing from a url to the user's default storage location.
+===========================================================================================================
+``agavepy.files.importData(filePath, systemId, callbackURL=None, fileName=None, fileToUpload=None, fileType=None, notifications=[], urlToIngest=None)``
+
+Parameters:
+-----------
+    * **systemId**: The unique id of the system on which the data resides. (string)
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **fileType**: The file format this file is in. Defaults to raw. This will be used in file transform operations. (string)
+    * **callbackURL**: The URI to notify when the import is complete. This can be an email address or http URL. If a URL is given, a GET will be made to this address. URL templating is supported. Valid template values are: ${NAME}, ${SOURCE_FORMAT}, ${DEST_FORMAT}, ${STATUS} (string)
+    * **fileName**: The name of the file after importing. If not specified, the uploaded file name will be used. (string)
+    * **urlToIngest**: The URL to import the file from. This parameter is used if not file is uploaded with this post. (string)
+    * **fileToUpload**: The file object to import. (void)
+    * **notifications**: A list of notification objects to apply to the transfer.  (FileNotificationRequest)
+
+
+Response:
+---------
+    * *A single RemoteFile object*
+
+**RemoteFile schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/RemoteFile.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "format": {
+          "description": "The file type of the file.", 
+          "type": "string"
+        }, 
+        "lastModified": {
+          "description": "The date this file was last modified in ISO 8601 format.", 
+          "type": "string"
+        }, 
+        "length": {
+          "description": "The length of the file/folder.", 
+          "type": "integer"
+        }, 
+        "mimeType": {
+          "description": "The mime type of the file/folder. If unknown, it defaults to application/binary.", 
+          "type": "string"
+        }, 
+        "name": {
+          "description": "The name of the file/folder.", 
+          "type": "string"
+        }, 
+        "path": {
+          "description": "The absolute path to the file/folder.", 
+          "type": "string"
+        }, 
+        "permissions": {
+          "description": "The system permission of the invoking user on the file/folder.", 
+          "type": "string"
+        }, 
+        "system": {
+          "description": "The systemId of the system where this file lives.", 
+          "type": "string"
+        }, 
+        "type": {
+          "description": "Whether it is a file or folder.", 
+          "type": "string"
+        }
+      }, 
+      "required": [], 
+      "title": "AgavePy RemoteFile schema", 
+      "type": "object"
+    }
+
+manage: Perform an action on a file or folder.
+==============================================
+``agavepy.files.manage(body, filePath, systemId)``
+
+Parameters:
+-----------
+    * **systemId**: The unique id of the system on which the data resides. (string)
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **body**: The operation to perform.  (JSON, FileOperationRequest)
+
+
+**FileOperationRequest schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/FileOperationRequest.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "action": {
+          "description": "Action to perform on the file or folder.", 
+          "enum": [
+            "mkdir", 
+            "rename", 
+            "copy", 
+            "move"
+          ], 
+          "type": "string"
+        }, 
+        "path": {
+          "description": "Destination file or folder.", 
+          "type": "string"
+        }
+      }, 
+      "required": [
+        "action"
+      ], 
+      "title": "AgavePy FileOperationRequest schema", 
+      "type": "object"
+    }
+
+Response:
+---------
+    * *String*
+
+listOnDefaultSystem: Get a remote directory listing.
+====================================================
+``agavepy.files.listOnDefaultSystem(filePath, limit=250, offset=0)``
+
+Parameters:
+-----------
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **limit**: The max number of results. (integer)
+    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
+
+
+Response:
+---------
+    * *Array of RemoteFile objects*
+
+**RemoteFile schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/RemoteFile.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "format": {
+          "description": "The file type of the file.", 
+          "type": "string"
+        }, 
+        "lastModified": {
+          "description": "The date this file was last modified in ISO 8601 format.", 
+          "type": "string"
+        }, 
+        "length": {
+          "description": "The length of the file/folder.", 
+          "type": "integer"
+        }, 
+        "mimeType": {
+          "description": "The mime type of the file/folder. If unknown, it defaults to application/binary.", 
+          "type": "string"
+        }, 
+        "name": {
+          "description": "The name of the file/folder.", 
+          "type": "string"
+        }, 
+        "path": {
+          "description": "The absolute path to the file/folder.", 
+          "type": "string"
+        }, 
+        "permissions": {
+          "description": "The system permission of the invoking user on the file/folder.", 
+          "type": "string"
+        }, 
+        "system": {
+          "description": "The systemId of the system where this file lives.", 
+          "type": "string"
+        }, 
+        "type": {
+          "description": "Whether it is a file or folder.", 
+          "type": "string"
+        }
+      }, 
+      "required": [], 
+      "title": "AgavePy RemoteFile schema", 
+      "type": "object"
+    }
+
+list: Get a remote directory listing on a specific system.
+==========================================================
+``agavepy.files.list(filePath, systemId, limit=250, offset=0)``
+
+Parameters:
+-----------
+    * **systemId**: The unique id of the system on which the data resides. (string)
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **limit**: The max number of results. (integer)
+    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
+
+
+Response:
+---------
+    * *Array of RemoteFile objects*
+
+**RemoteFile schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/RemoteFile.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "format": {
+          "description": "The file type of the file.", 
+          "type": "string"
+        }, 
+        "lastModified": {
+          "description": "The date this file was last modified in ISO 8601 format.", 
+          "type": "string"
+        }, 
+        "length": {
+          "description": "The length of the file/folder.", 
+          "type": "integer"
+        }, 
+        "mimeType": {
+          "description": "The mime type of the file/folder. If unknown, it defaults to application/binary.", 
+          "type": "string"
+        }, 
+        "name": {
+          "description": "The name of the file/folder.", 
+          "type": "string"
+        }, 
+        "path": {
+          "description": "The absolute path to the file/folder.", 
+          "type": "string"
+        }, 
+        "permissions": {
+          "description": "The system permission of the invoking user on the file/folder.", 
+          "type": "string"
+        }, 
+        "system": {
+          "description": "The systemId of the system where this file lives.", 
+          "type": "string"
+        }, 
+        "type": {
+          "description": "Whether it is a file or folder.", 
+          "type": "string"
+        }
+      }, 
+      "required": [], 
+      "title": "AgavePy RemoteFile schema", 
+      "type": "object"
+    }
+
+getHistoryOnDefaultSystem: Download a file from the user's default storage location.
+====================================================================================
+``agavepy.files.getHistoryOnDefaultSystem(filePath, limit=250, offset=0)``
+
+Parameters:
+-----------
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **limit**: The max number of results. (integer)
+    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
+
+
+Response:
+---------
+    * *Array of FileHistory objects*
+
+**FileHistory schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/FileHistory.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "created": {
+          "description": "The date of the event.", 
+          "type": "string"
+        }, 
+        "description": {
+          "description": "A brief description of the event details.", 
+          "type": "String"
+        }, 
+        "status": {
+          "description": "The status of the file/folder after this event.", 
+          "type": "String"
+        }
+      }, 
+      "required": [], 
+      "title": "AgavePy FileHistory schema", 
+      "type": "object"
+    }
+
+getHistory: Return history of api actions.
+==========================================
+``agavepy.files.getHistory(filePath, systemId, limit=250, offset=0)``
+
+Parameters:
+-----------
+    * **systemId**: The unique id of the system on which the data resides. (string)
+    * **filePath**: The path of the file relative to the given system root location. (string)
+    * **limit**: The max number of results. (integer)
+    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
+
+
+Response:
+---------
+    * *Array of FileHistory objects*
+
+**FileHistory schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/FileHistory.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "created": {
+          "description": "The date of the event.", 
+          "type": "string"
+        }, 
+        "description": {
+          "description": "A brief description of the event details.", 
+          "type": "String"
+        }, 
+        "status": {
+          "description": "The status of the file/folder after this event.", 
+          "type": "String"
+        }
+      }, 
+      "required": [], 
+      "title": "AgavePy FileHistory schema", 
+      "type": "object"
+    }
+
+listPermissionsOnDefaultSystem: List all the share permissions for a file or folder.
+====================================================================================
+``agavepy.files.listPermissionsOnDefaultSystem(filePath, limit=250, offset=0)``
+
+Parameters:
+-----------
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **limit**: The max number of results. (integer)
+    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
+
+
+Response:
+---------
+    * *Array of FilePermission objects*
+
+**FilePermission schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermission.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "name": {
+          "description": "The name of the file/folder.", 
+          "type": "string"
+        }, 
+        "owner": {
+          "description": "Local username of the owner.", 
+          "type": "string"
+        }, 
+        "permissions": {
+          "description": "One or more permission objects", 
+          "type": "array"
+        }
+      }, 
+      "required": [], 
+      "title": "AgavePy FilePermission schema", 
+      "type": "object"
+    }
+
+updatePermissionsOnDefaultSystem: Update permissions for a single user.
+=======================================================================
+``agavepy.files.updatePermissionsOnDefaultSystem(body, filePath)``
+
+Parameters:
+-----------
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **body**: The permission add or update.  (JSON, FilePermissionRequest)
+
+
+**FilePermissionRequest schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermissionRequest.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "permission": {
+          "description": "The permission to set", 
+          "enum": [
+            "READ", 
+            "WRITE", 
+            "EXECUTE", 
+            "READ_WRITE", 
+            "READ_EXECUTE", 
+            "WRITE_EXECUTE", 
+            "ALL", 
+            "NONE"
+          ], 
+          "type": "string"
+        }, 
+        "recursive": {
+          "description": "Should updated permissions be applied recursively. Defaults to false.", 
+          "type": "boolean"
+        }, 
+        "username": {
+          "description": "The username of the api user whose permission is to be set.", 
+          "type": "string"
+        }
+      }, 
+      "required": [
+        "username", 
+        "permission"
+      ], 
+      "title": "AgavePy FilePermissionRequest schema", 
+      "type": "object"
+    }
+
+Response:
+---------
+    * *String*
+
+deletePermissions: Deletes all permissions on a file except those of the owner.
+===============================================================================
+``agavepy.files.deletePermissions(filePath, systemId)``
+
+Parameters:
+-----------
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **systemId**: The unique id of the system on which the data resides. (string)
+
+
+Response:
+---------
+    * *String*
+
+listPermissions: List all the share permissions for a file or folder.
+=====================================================================
+``agavepy.files.listPermissions(filePath, systemId, limit=250, offset=0)``
+
+Parameters:
+-----------
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **limit**: The max number of results. (integer)
+    * **systemId**: The unique id of the system on which the data resides. (string)
+    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
+
+
+Response:
+---------
+    * *Array of FilePermission objects*
+
+**FilePermission schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermission.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "name": {
+          "description": "The name of the file/folder.", 
+          "type": "string"
+        }, 
+        "owner": {
+          "description": "Local username of the owner.", 
+          "type": "string"
+        }, 
+        "permissions": {
+          "description": "One or more permission objects", 
+          "type": "array"
+        }
+      }, 
+      "required": [], 
+      "title": "AgavePy FilePermission schema", 
+      "type": "object"
+    }
+
+updatePermissions: Update permissions for a single user.
+========================================================
+``agavepy.files.updatePermissions(body, filePath, systemId)``
+
+Parameters:
+-----------
+    * **filePath**: The path of the file relative to the user's default storage location. (string)
+    * **systemId**: The unique id of the system on which the data resides. (string)
+    * **body**: The permission add or update.  (JSON, FilePermissionRequest)
+
+
+**FilePermissionRequest schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermissionRequest.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "permission": {
+          "description": "The permission to set", 
+          "enum": [
+            "READ", 
+            "WRITE", 
+            "EXECUTE", 
+            "READ_WRITE", 
+            "READ_EXECUTE", 
+            "WRITE_EXECUTE", 
+            "ALL", 
+            "NONE"
+          ], 
+          "type": "string"
+        }, 
+        "recursive": {
+          "description": "Should updated permissions be applied recursively. Defaults to false.", 
+          "type": "boolean"
+        }, 
+        "username": {
+          "description": "The username of the api user whose permission is to be set.", 
+          "type": "string"
+        }
+      }, 
+      "required": [
+        "username", 
+        "permission"
+      ], 
+      "title": "AgavePy FilePermissionRequest schema", 
+      "type": "object"
+    }
+
+Response:
+---------
+    * *Array of FilePermission objects*
+
+**FilePermission schema**
+
+.. code-block:: javascript
+
+    {
+      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermission.json", 
+      "$schema": "http://json-schema.org/draft-07/schema#", 
+      "properties": {
+        "name": {
+          "description": "The name of the file/folder.", 
+          "type": "string"
+        }, 
+        "owner": {
+          "description": "Local username of the owner.", 
+          "type": "string"
+        }, 
+        "permissions": {
+          "description": "One or more permission objects", 
+          "type": "array"
+        }
+      }, 
+      "required": [], 
+      "title": "AgavePy FilePermission schema", 
+      "type": "object"
+    }
+
diff --git a/docs/docsite/files/files.rst b/docs/docsite/files/files.rst
index 096dbf1..a598f43 100644
--- a/docs/docsite/files/files.rst
+++ b/docs/docsite/files/files.rst
@@ -1,731 +1,21 @@
-*****
-Files
-*****
+.. _files:
 
-Summary: Move and manage data
+##################
+Working with files
+##################
 
-deleteFromDefaultSystem: Deletes a file or folder.
-==================================================
-``agavepy.files.deleteFromDefaultSystem(sourcefilePath)``
+If you have already initiated a session, `authentication`, and have a storage
+system setup then you start working with files.
 
-Parameters:
------------
-    * **sourcefilePath**: The path of the file relative to the user's default storage location. (string)
+Downloading a file
+##################
 
+Let's say you have a file name ``SP1.fq`` on your remote storage system which
+is called ``tacc-globalfs-username``.
 
-Response:
----------
-    * *String*
+You can download the file ``SP1.fq`` to your host and specify the location of
+where you want to store the file (i.e., ``/opt/data/SP1.fq``) as such:
 
-downloadFromDefaultSystem: Download a file from the user's default storage location.
-====================================================================================
-``agavepy.files.downloadFromDefaultSystem(sourcefilePath)``
-
-Parameters:
------------
-    * **sourcefilePath**: The path of the file relative to the user's default storage location. (string)
-
-
-Response:
----------
-    * *None*
-
-importToDefaultSystem: Import a file via direct upload or importing from a url to the user's default storage location.
-======================================================================================================================
-``agavepy.files.importToDefaultSystem(sourcefilePath, callbackURL=None, fileName=None, fileToUpload=None, fileType=None, urlToIngest=None)``
-
-Parameters:
------------
-    * **sourcefilePath**: The path of the file relative to the user's default storage location. (string)
-    * **fileType**: The file format this file is in. Defaults to raw. This will be used in file transform operations. (string)
-    * **callbackURL**: The URI to notify when the import is complete. This can be an email address or http URL. If a URL is given, a GET will be made to this address. URL templating is supported. Valid template values are: ${NAME}, ${SOURCE_FORMAT}, ${DEST_FORMAT}, ${STATUS} (string)
-    * **fileName**: The name of the file after importing. If not specified, the uploaded file name will be used. (string)
-    * **urlToIngest**: The URL to import the file from. This parameter is used if not file is uploaded with this post. (string)
-    * **fileToUpload**: The file object to import. (void)
-
-
-Response:
----------
-    * *A single RemoteFile object*
-
-**RemoteFile schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/RemoteFile.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "format": {
-          "description": "The file type of the file.", 
-          "type": "string"
-        }, 
-        "lastModified": {
-          "description": "The date this file was last modified in ISO 8601 format.", 
-          "type": "string"
-        }, 
-        "length": {
-          "description": "The length of the file/folder.", 
-          "type": "integer"
-        }, 
-        "mimeType": {
-          "description": "The mime type of the file/folder. If unknown, it defaults to application/binary.", 
-          "type": "string"
-        }, 
-        "name": {
-          "description": "The name of the file/folder.", 
-          "type": "string"
-        }, 
-        "path": {
-          "description": "The absolute path to the file/folder.", 
-          "type": "string"
-        }, 
-        "permissions": {
-          "description": "The system permission of the invoking user on the file/folder.", 
-          "type": "string"
-        }, 
-        "system": {
-          "description": "The systemId of the system where this file lives.", 
-          "type": "string"
-        }, 
-        "type": {
-          "description": "Whether it is a file or folder.", 
-          "type": "string"
-        }
-      }, 
-      "required": [], 
-      "title": "AgavePy RemoteFile schema", 
-      "type": "object"
-    }
-
-manageOnDefaultSystem: Perform an action on a file or folder.
-=============================================================
-``agavepy.files.manageOnDefaultSystem(body, sourcefilePath)``
-
-Parameters:
------------
-    * **sourcefilePath**: The path of the file relative to the user's default storage location. (string)
-    * **body**: The operation to perform.  (JSON, FileOperationRequest)
-
-
-**FileOperationRequest schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/FileOperationRequest.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "action": {
-          "description": "Action to perform on the file or folder.", 
-          "enum": [
-            "mkdir", 
-            "rename", 
-            "copy", 
-            "move"
-          ], 
-          "type": "string"
-        }, 
-        "path": {
-          "description": "Destination file or folder.", 
-          "type": "string"
-        }
-      }, 
-      "required": [
-        "action"
-      ], 
-      "title": "AgavePy FileOperationRequest schema", 
-      "type": "object"
-    }
-
-Response:
----------
-    * *String*
-
-delete: Deletes a file or folder.
-=================================
-``agavepy.files.delete(filePath, systemId)``
-
-Parameters:
------------
-    * **systemId**: The unique id of the system on which the data resides. (string)
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-
-
-Response:
----------
-    * *String*
-
-download: Download a file from the user's default storage location.
-===================================================================
-``agavepy.files.download(filePath, systemId)``
-
-Parameters:
------------
-    * **systemId**: The unique id of the system on which the data resides. (string)
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-
-
-Response:
----------
-    * *None*
-
-importData: Import a file via direct upload or importing from a url to the user's default storage location.
-===========================================================================================================
-``agavepy.files.importData(filePath, systemId, callbackURL=None, fileName=None, fileToUpload=None, fileType=None, notifications=[], urlToIngest=None)``
-
-Parameters:
------------
-    * **systemId**: The unique id of the system on which the data resides. (string)
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **fileType**: The file format this file is in. Defaults to raw. This will be used in file transform operations. (string)
-    * **callbackURL**: The URI to notify when the import is complete. This can be an email address or http URL. If a URL is given, a GET will be made to this address. URL templating is supported. Valid template values are: ${NAME}, ${SOURCE_FORMAT}, ${DEST_FORMAT}, ${STATUS} (string)
-    * **fileName**: The name of the file after importing. If not specified, the uploaded file name will be used. (string)
-    * **urlToIngest**: The URL to import the file from. This parameter is used if not file is uploaded with this post. (string)
-    * **fileToUpload**: The file object to import. (void)
-    * **notifications**: A list of notification objects to apply to the transfer.  (FileNotificationRequest)
-
-
-Response:
----------
-    * *A single RemoteFile object*
-
-**RemoteFile schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/RemoteFile.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "format": {
-          "description": "The file type of the file.", 
-          "type": "string"
-        }, 
-        "lastModified": {
-          "description": "The date this file was last modified in ISO 8601 format.", 
-          "type": "string"
-        }, 
-        "length": {
-          "description": "The length of the file/folder.", 
-          "type": "integer"
-        }, 
-        "mimeType": {
-          "description": "The mime type of the file/folder. If unknown, it defaults to application/binary.", 
-          "type": "string"
-        }, 
-        "name": {
-          "description": "The name of the file/folder.", 
-          "type": "string"
-        }, 
-        "path": {
-          "description": "The absolute path to the file/folder.", 
-          "type": "string"
-        }, 
-        "permissions": {
-          "description": "The system permission of the invoking user on the file/folder.", 
-          "type": "string"
-        }, 
-        "system": {
-          "description": "The systemId of the system where this file lives.", 
-          "type": "string"
-        }, 
-        "type": {
-          "description": "Whether it is a file or folder.", 
-          "type": "string"
-        }
-      }, 
-      "required": [], 
-      "title": "AgavePy RemoteFile schema", 
-      "type": "object"
-    }
-
-manage: Perform an action on a file or folder.
-==============================================
-``agavepy.files.manage(body, filePath, systemId)``
-
-Parameters:
------------
-    * **systemId**: The unique id of the system on which the data resides. (string)
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **body**: The operation to perform.  (JSON, FileOperationRequest)
-
-
-**FileOperationRequest schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/FileOperationRequest.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "action": {
-          "description": "Action to perform on the file or folder.", 
-          "enum": [
-            "mkdir", 
-            "rename", 
-            "copy", 
-            "move"
-          ], 
-          "type": "string"
-        }, 
-        "path": {
-          "description": "Destination file or folder.", 
-          "type": "string"
-        }
-      }, 
-      "required": [
-        "action"
-      ], 
-      "title": "AgavePy FileOperationRequest schema", 
-      "type": "object"
-    }
-
-Response:
----------
-    * *String*
-
-listOnDefaultSystem: Get a remote directory listing.
-====================================================
-``agavepy.files.listOnDefaultSystem(filePath, limit=250, offset=0)``
-
-Parameters:
------------
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **limit**: The max number of results. (integer)
-    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
-
-
-Response:
----------
-    * *Array of RemoteFile objects*
-
-**RemoteFile schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/RemoteFile.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "format": {
-          "description": "The file type of the file.", 
-          "type": "string"
-        }, 
-        "lastModified": {
-          "description": "The date this file was last modified in ISO 8601 format.", 
-          "type": "string"
-        }, 
-        "length": {
-          "description": "The length of the file/folder.", 
-          "type": "integer"
-        }, 
-        "mimeType": {
-          "description": "The mime type of the file/folder. If unknown, it defaults to application/binary.", 
-          "type": "string"
-        }, 
-        "name": {
-          "description": "The name of the file/folder.", 
-          "type": "string"
-        }, 
-        "path": {
-          "description": "The absolute path to the file/folder.", 
-          "type": "string"
-        }, 
-        "permissions": {
-          "description": "The system permission of the invoking user on the file/folder.", 
-          "type": "string"
-        }, 
-        "system": {
-          "description": "The systemId of the system where this file lives.", 
-          "type": "string"
-        }, 
-        "type": {
-          "description": "Whether it is a file or folder.", 
-          "type": "string"
-        }
-      }, 
-      "required": [], 
-      "title": "AgavePy RemoteFile schema", 
-      "type": "object"
-    }
-
-list: Get a remote directory listing on a specific system.
-==========================================================
-``agavepy.files.list(filePath, systemId, limit=250, offset=0)``
-
-Parameters:
------------
-    * **systemId**: The unique id of the system on which the data resides. (string)
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **limit**: The max number of results. (integer)
-    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
-
-
-Response:
----------
-    * *Array of RemoteFile objects*
-
-**RemoteFile schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/RemoteFile.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "format": {
-          "description": "The file type of the file.", 
-          "type": "string"
-        }, 
-        "lastModified": {
-          "description": "The date this file was last modified in ISO 8601 format.", 
-          "type": "string"
-        }, 
-        "length": {
-          "description": "The length of the file/folder.", 
-          "type": "integer"
-        }, 
-        "mimeType": {
-          "description": "The mime type of the file/folder. If unknown, it defaults to application/binary.", 
-          "type": "string"
-        }, 
-        "name": {
-          "description": "The name of the file/folder.", 
-          "type": "string"
-        }, 
-        "path": {
-          "description": "The absolute path to the file/folder.", 
-          "type": "string"
-        }, 
-        "permissions": {
-          "description": "The system permission of the invoking user on the file/folder.", 
-          "type": "string"
-        }, 
-        "system": {
-          "description": "The systemId of the system where this file lives.", 
-          "type": "string"
-        }, 
-        "type": {
-          "description": "Whether it is a file or folder.", 
-          "type": "string"
-        }
-      }, 
-      "required": [], 
-      "title": "AgavePy RemoteFile schema", 
-      "type": "object"
-    }
-
-getHistoryOnDefaultSystem: Download a file from the user's default storage location.
-====================================================================================
-``agavepy.files.getHistoryOnDefaultSystem(filePath, limit=250, offset=0)``
-
-Parameters:
------------
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **limit**: The max number of results. (integer)
-    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
-
-
-Response:
----------
-    * *Array of FileHistory objects*
-
-**FileHistory schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/FileHistory.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "created": {
-          "description": "The date of the event.", 
-          "type": "string"
-        }, 
-        "description": {
-          "description": "A brief description of the event details.", 
-          "type": "String"
-        }, 
-        "status": {
-          "description": "The status of the file/folder after this event.", 
-          "type": "String"
-        }
-      }, 
-      "required": [], 
-      "title": "AgavePy FileHistory schema", 
-      "type": "object"
-    }
-
-getHistory: Return history of api actions.
-==========================================
-``agavepy.files.getHistory(filePath, systemId, limit=250, offset=0)``
-
-Parameters:
------------
-    * **systemId**: The unique id of the system on which the data resides. (string)
-    * **filePath**: The path of the file relative to the given system root location. (string)
-    * **limit**: The max number of results. (integer)
-    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
-
-
-Response:
----------
-    * *Array of FileHistory objects*
-
-**FileHistory schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/FileHistory.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "created": {
-          "description": "The date of the event.", 
-          "type": "string"
-        }, 
-        "description": {
-          "description": "A brief description of the event details.", 
-          "type": "String"
-        }, 
-        "status": {
-          "description": "The status of the file/folder after this event.", 
-          "type": "String"
-        }
-      }, 
-      "required": [], 
-      "title": "AgavePy FileHistory schema", 
-      "type": "object"
-    }
-
-listPermissionsOnDefaultSystem: List all the share permissions for a file or folder.
-====================================================================================
-``agavepy.files.listPermissionsOnDefaultSystem(filePath, limit=250, offset=0)``
-
-Parameters:
------------
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **limit**: The max number of results. (integer)
-    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
-
-
-Response:
----------
-    * *Array of FilePermission objects*
-
-**FilePermission schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermission.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "name": {
-          "description": "The name of the file/folder.", 
-          "type": "string"
-        }, 
-        "owner": {
-          "description": "Local username of the owner.", 
-          "type": "string"
-        }, 
-        "permissions": {
-          "description": "One or more permission objects", 
-          "type": "array"
-        }
-      }, 
-      "required": [], 
-      "title": "AgavePy FilePermission schema", 
-      "type": "object"
-    }
-
-updatePermissionsOnDefaultSystem: Update permissions for a single user.
-=======================================================================
-``agavepy.files.updatePermissionsOnDefaultSystem(body, filePath)``
-
-Parameters:
------------
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **body**: The permission add or update.  (JSON, FilePermissionRequest)
-
-
-**FilePermissionRequest schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermissionRequest.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "permission": {
-          "description": "The permission to set", 
-          "enum": [
-            "READ", 
-            "WRITE", 
-            "EXECUTE", 
-            "READ_WRITE", 
-            "READ_EXECUTE", 
-            "WRITE_EXECUTE", 
-            "ALL", 
-            "NONE"
-          ], 
-          "type": "string"
-        }, 
-        "recursive": {
-          "description": "Should updated permissions be applied recursively. Defaults to false.", 
-          "type": "boolean"
-        }, 
-        "username": {
-          "description": "The username of the api user whose permission is to be set.", 
-          "type": "string"
-        }
-      }, 
-      "required": [
-        "username", 
-        "permission"
-      ], 
-      "title": "AgavePy FilePermissionRequest schema", 
-      "type": "object"
-    }
-
-Response:
----------
-    * *String*
-
-deletePermissions: Deletes all permissions on a file except those of the owner.
-===============================================================================
-``agavepy.files.deletePermissions(filePath, systemId)``
-
-Parameters:
------------
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **systemId**: The unique id of the system on which the data resides. (string)
-
-
-Response:
----------
-    * *String*
-
-listPermissions: List all the share permissions for a file or folder.
-=====================================================================
-``agavepy.files.listPermissions(filePath, systemId, limit=250, offset=0)``
-
-Parameters:
------------
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **limit**: The max number of results. (integer)
-    * **systemId**: The unique id of the system on which the data resides. (string)
-    * **offset**: The number of records to when returning the results. When paginating results, the page number = ceil(offset/limit) (integer)
-
-
-Response:
----------
-    * *Array of FilePermission objects*
-
-**FilePermission schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermission.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "name": {
-          "description": "The name of the file/folder.", 
-          "type": "string"
-        }, 
-        "owner": {
-          "description": "Local username of the owner.", 
-          "type": "string"
-        }, 
-        "permissions": {
-          "description": "One or more permission objects", 
-          "type": "array"
-        }
-      }, 
-      "required": [], 
-      "title": "AgavePy FilePermission schema", 
-      "type": "object"
-    }
-
-updatePermissions: Update permissions for a single user.
-========================================================
-``agavepy.files.updatePermissions(body, filePath, systemId)``
-
-Parameters:
------------
-    * **filePath**: The path of the file relative to the user's default storage location. (string)
-    * **systemId**: The unique id of the system on which the data resides. (string)
-    * **body**: The permission add or update.  (JSON, FilePermissionRequest)
-
-
-**FilePermissionRequest schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermissionRequest.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "permission": {
-          "description": "The permission to set", 
-          "enum": [
-            "READ", 
-            "WRITE", 
-            "EXECUTE", 
-            "READ_WRITE", 
-            "READ_EXECUTE", 
-            "WRITE_EXECUTE", 
-            "ALL", 
-            "NONE"
-          ], 
-          "type": "string"
-        }, 
-        "recursive": {
-          "description": "Should updated permissions be applied recursively. Defaults to false.", 
-          "type": "boolean"
-        }, 
-        "username": {
-          "description": "The username of the api user whose permission is to be set.", 
-          "type": "string"
-        }
-      }, 
-      "required": [
-        "username", 
-        "permission"
-      ], 
-      "title": "AgavePy FilePermissionRequest schema", 
-      "type": "object"
-    }
-
-Response:
----------
-    * *Array of FilePermission objects*
-
-**FilePermission schema**
-
-.. code-block:: javascript
-
-    {
-      "$id": "http://agavepy.readthedocs.io/en/latest/FilePermission.json", 
-      "$schema": "http://json-schema.org/draft-07/schema#", 
-      "properties": {
-        "name": {
-          "description": "The name of the file/folder.", 
-          "type": "string"
-        }, 
-        "owner": {
-          "description": "Local username of the owner.", 
-          "type": "string"
-        }, 
-        "permissions": {
-          "description": "One or more permission objects", 
-          "type": "array"
-        }
-      }, 
-      "required": [], 
-      "title": "AgavePy FilePermission schema", 
-      "type": "object"
-    }
+.. code-block:: pycon
 
+    >>> agave.files_download("tacc-globalfs-username/SP1.fq", "/opt/data/SP1.fq")
diff --git a/docs/docsite/files/index.rst b/docs/docsite/files/index.rst
index 108df29..b7aba32 100644
--- a/docs/docsite/files/index.rst
+++ b/docs/docsite/files/index.rst
@@ -11,6 +11,7 @@ to use, to share with collaborators, or to setup for long term storage.
    :maxdepth: 2                                                                 
 
    files
+   files-listing
 
 .. only::  subproject and html                                                  
                                                                                 


Test patch:
diff --git a/tests/files_test.py b/tests/files_test.py
new file mode 100644
index 0000000..4c0c2fa
--- /dev/null
+++ b/tests/files_test.py
@@ -0,0 +1,142 @@
+"""
+    files_tests.py
+"""
+import pytest
+import cgi
+import filecmp
+import json
+import mimetypes
+import os
+import shutil
+import tempfile
+import time
+from testsuite_utils import MockServer                                          
+from response_templates import response_template_to_json
+
+try: # python 2
+    from BaseHTTPServer import BaseHTTPRequestHandler
+except ImportError: # python 3
+    from http.server import BaseHTTPRequestHandler
+try:
+    from unittest.mock import patch
+except ImportError:
+    from mock import patch
+
+from agavepy.agave import Agave
+
+
+
+class MockServerFilesEndpoints(BaseHTTPRequestHandler):
+    def send_headers(self):
+        # The path corresponds to the service being queried. For example,
+        # querying "http://localhost:80/f/file.txt" will set the path as
+        # "/f/file.txt". 
+        npath = os.path.normpath(self.path)
+        npath = npath[1:]
+        path_elements = npath.split("/")
+
+        # Check access token.
+        try: # Python 2
+            valid_access_token = self.headers.getheader("Authorization")
+        except AttributeError: # Python 3
+            valid_access_token = self.headers.get("Authorization")
+        if not valid_access_token:
+            self.send_error(401, "Unauthorized")
+            return None
+
+        # Send headers for /files/v2/media/system requests.
+        if "/files/v2/media/system" in self.path: 
+            self.send_response(200)
+            self.send_header("Content-Type", "text/json; character=utf-8")
+            self.end_headers()
+        else:
+            self.send_error(404, "fuck")
+            return None
+    
+        return path_elements
+
+
+    def do_GET(self):
+        """ Get a file
+
+        This route responds with the contents of a file. The file is expected
+        to be in the current working directory and is expected to be created by
+        the test setup.
+        """
+        # 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:
+            return
+
+        # Send contents of file to wfile object.
+        filename = elements[-1]
+        if "?pretty=true" in filename: filename = filename[:-len("?pretty=true")]
+        with open(filename, "rb") as f:
+            shutil.copyfileobj(f, self.wfile)
+
+
+
+
+
+class TestMockServer(MockServer):
+    """ Test client-related agave api endpoints
+
+    Tests client creation (HTTP POST request), removal (HTTP DELETE request),
+    and listing (HTTP GET request).
+    """
+
+    @classmethod
+    def setup_class(cls):
+        """ Set up an agave mock server
+
+        Listen and serve mock api as a daemon.
+        """
+        MockServer.serve.__func__(cls, MockServerFilesEndpoints)
+
+    def create_local_dummy_file(self):
+        """ Create a dummy file for testing
+
+        Create a dummy file to test file copying with the cli. Emulate a file
+        on a local system.
+        """
+        _, file_path = tempfile.mkstemp()
+        try:
+            with open(file_path, "w") as tmp:
+                tmp.write("this is a test file\n")
+        except:
+            os.remove(file_path)
+
+        return file_path
+
+    def create_remote_dummy_file(self):
+        """ Create a dummy file for testing
+
+        Create a dummy file to test file copying with the cli. Emulate a file
+        on a remote agave system.
+        """
+        tmp_filename = "thisisatest.txt"
+        with open(tmp_filename, "w") as tmp:
+            tmp.write("this emulates a file on a remote system\n")
+        return tmp_filename
+
+    def test_files_download(self):
+        """ Test file copying from a remote to the local system
+        """
+        # Create a "remote" dummy file.
+        tmp_file = self.create_remote_dummy_file()
+
+        # Destination of downloaded file.
+        local_file = "testfile.txt"
+
+        try:
+            local_uri = "http://localhost:{port}/".format(port=self.mock_server_port)
+            agave = Agave(api_server=local_uri)
+            agave.token = "mock-access-token"
+
+            agave.files_download("tacc-globalfs-user/"+tmp_file, local_file)
+        finally:
+            assert filecmp.cmp(tmp_file, local_file)
+
+            # rm dummy file in current working directory.
+            if os.path.exists(tmp_file):
+                os.remove(tmp_file)