Skip to content

Commit

Permalink
feat(fusion-sql): add support for Files API (personal and shared spaces)
Browse files Browse the repository at this point in the history
  • Loading branch information
nunogoncalves03 committed Dec 2, 2024
1 parent 6ed1217 commit c33007a
Show file tree
Hide file tree
Showing 3 changed files with 423 additions and 0 deletions.
6 changes: 6 additions & 0 deletions singlestoredb/fusion/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@
'<column>': '',
'<catalog-name>': '',
'<link-name>': '',
'<file-location>': r'''
file_location = { PERSONAL | SHARED }
''',
'<file-type>': r'''
file_type = { FILE | FOLDER }
''',
}

BUILTIN_DEFAULTS = { # type: ignore
Expand Down
379 changes: 379 additions & 0 deletions singlestoredb/fusion/handlers/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
#!/usr/bin/env python3
from typing import Any
from typing import Dict
from typing import Optional

from .. import result
from ..handler import SQLHandler
from ..result import FusionSQLResult
from .utils import dt_isoformat
from .utils import get_file_space


class ShowFilesHandler(SQLHandler):
"""
SHOW <file-location> FILES
[ at_path ] [ <like> ]
[ <order-by> ]
[ <limit> ] [ recursive ] [ extended ];
# File path to list
at_path = AT '<path>'
# Should the listing be recursive?
recursive = RECURSIVE
# Should extended attributes be shown?
extended = EXTENDED
Description
-----------
Displays a list of files in a personal/shared space.
Arguments
---------
* ``<file-location>``: The location of the file, it can
be either 'PERSONAL' or 'SHARED'.
* ``<path>``: A path in the personal/shared space.
* ``<pattern>``: A pattern similar to SQL LIKE clause.
Uses ``%`` as the wildcard character.
Remarks
-------
* Use the ``LIKE`` clause to specify a pattern and return only the
files that match the specified pattern.
* The ``LIMIT`` clause limits the number of results to the
specified number.
* Use the ``ORDER BY`` clause to sort the results by the specified
key. By default, the results are sorted in the ascending order.
* The ``AT PATH`` clause specifies the path in the personal/shared
space to list the files from.
* Use the ``RECURSIVE`` clause to list the files recursively.
* To return more information about the files, use the ``EXTENDED``
clause.
Examples
--------
The following commands list the files at a specific path::
SHOW PERSONAL FILES AT PATH "/data/";
SHOW SHARED FILES AT PATH "/data/";
The following commands list the files recursively with
additional information::
SHOW PERSONAL FILES RECURSIVE EXTENDED;
SHOW SHARED FILES RECURSIVE EXTENDED;
See Also
--------
* ``UPLOAD PERSONAL FILE``
* ``UPLOAD SHARED FILE``
* ``DOWNLOAD PERSONAL FILE``
* ``DOWNLOAD SHARED FILE``
""" # noqa: E501

def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
file_space = get_file_space(params)

res = FusionSQLResult()
res.add_field('Name', result.STRING)

if params['extended']:
res.add_field('Type', result.STRING)
res.add_field('Size', result.INTEGER)
res.add_field('Writable', result.STRING)
res.add_field('CreatedAt', result.DATETIME)
res.add_field('LastModifiedAt', result.DATETIME)

files = []
for x in file_space.listdir(
params['at_path'] or '/',
recursive=params['recursive'],
):
info = file_space.info(x)
files.append(
tuple([
x, info.type, info.size or 0, info.writable,
dt_isoformat(info.created_at),
dt_isoformat(info.last_modified_at),
]),
)
res.set_rows(files)

else:
res.set_rows([(x,) for x in file_space.listdir(
params['at_path'] or '/',
recursive=params['recursive'],
)])

if params['like']:
res = res.like(Name=params['like'])

return res.order_by(**params['order_by']).limit(params['limit'])


ShowFilesHandler.register(overwrite=True)


class UploadFileHandler(SQLHandler):
"""
UPLOAD <file-location> FILE TO path
FROM local_path [ overwrite ];
# Path to file
path = '<path>'
# Path to local file
local_path = '<local-path>'
# Should an existing file be overwritten?
overwrite = OVERWRITE
Description
-----------
Uploads a file to a personal/shared space.
Arguments
---------
* ``<file-location>``: The location of the file, it can
be either 'PERSONAL' or 'SHARED'.
* ``<path>``: The path in the personal/shared space where the file is uploaded.
* ``<local-path>``: The path to the file to upload in the local
directory.
Remarks
-------
* If the ``OVERWRITE`` clause is specified, any existing file at the
specified path in the personal/shared space is overwritten.
Examples
--------
The following commands upload a file to a personal/shared space and overwrite any
existing files at the specified path::
UPLOAD PERSONAL FILE TO '/data/stats.csv'
FROM '/tmp/user/stats.csv' OVERWRITE;
UPLOAD SHARED FILE TO '/data/stats.csv'
FROM '/tmp/user/stats.csv' OVERWRITE;
See Also
--------
* ``DOWNLOAD PERSONAL FILE``
* ``DOWNLOAD SHARED FILE``
""" # noqa: E501

def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
file_space = get_file_space(params)
file_space.upload_file(
params['local_path'], params['path'],
overwrite=params['overwrite'],
)
return None


UploadFileHandler.register(overwrite=True)


class DownloadFileHandler(SQLHandler):
"""
DOWNLOAD <file-location> FILE path
[ local_path ]
[ overwrite ]
[ encoding ];
# Path to file
path = '<path>'
# Path to local file
local_path = TO '<local-path>'
# Should an existing file be overwritten?
overwrite = OVERWRITE
# File encoding
encoding = ENCODING '<encoding>'
Description
-----------
Download a file from a personal/shared space.
Arguments
---------
* ``<file-location>``: The location of the file, it can
be either 'PERSONAL' or 'SHARED'.
* ``<path>``: The path to the file to download in a personal/shared space.
* ``<encoding>``: The encoding to apply to the downloaded file.
* ``<local-path>``: Specifies the path in the local directory
where the file is downloaded.
Remarks
-------
* If the ``OVERWRITE`` clause is specified, any existing file at
the download location is overwritten.
* By default, files are downloaded in binary encoding. To view
the contents of the file on the standard output, use the
``ENCODING`` clause and specify an encoding.
* If ``<local-path>`` is not specified, the file is displayed
on the standard output.
Examples
--------
The following commands display the contents of the file on the
standard output::
DOWNLOAD PERSONAL FILE '/data/stats.csv' ENCODING 'utf8';
DOWNLOAD SHARED FILE '/data/stats.csv' ENCODING 'utf8';
The following commands download a file to a specific location and
overwrites any existing file with the name ``stats.csv`` on the local storage::
DOWNLOAD PERSONAL FILE '/data/stats.csv'
TO '/tmp/data.csv' OVERWRITE;
DOWNLOAD SHARED FILE '/data/stats.csv'
TO '/tmp/data.csv' OVERWRITE;
See Also
--------
* ``UPLOAD PERSONAL FILE``
* ``UPLOAD SHARED FILE``
""" # noqa: E501

def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
file_space = get_file_space(params)

out = file_space.download_file(
params['path'],
local_path=params['local_path'] or None,
overwrite=params['overwrite'],
encoding=params['encoding'] or None,
)

if not params['local_path']:
res = FusionSQLResult()
if params['encoding']:
res.add_field('Data', result.STRING)
else:
res.add_field('Data', result.BLOB)
res.set_rows([(out,)])
return res

return None


DownloadFileHandler.register(overwrite=True)


class DropHandler(SQLHandler):
"""
DROP <file-location> <file-type> path
[ recursive ];
# Path to file
path = '<path>'
# Should folders be deleted recursively?
recursive = RECURSIVE
Description
-----------
Deletes a file/folder from a personal/shared space.
Arguments
---------
* ``<file-location>``: The location of the file, it can
be either 'PERSONAL' or 'SHARED'.
* ``<file-type>``: The type of the file, it can
be either 'FILE' or 'FOLDER'.
* ``<path>``: The path to the file to delete in a personal/shared space.
Remarks
-------
* The ``RECURSIVE`` clause indicates that the specified folder
is deleted recursively.
Example
--------
The following commands delete a file/folder from a personal/shared space::
DROP PERSONAL FILE '/data/stats.csv';
DROP SHARED FILE '/data/stats.csv';
DROP PERSONAL FOLDER '/data/' RECURSIVE;
DROP SHARED FOLDER '/data/' RECURSIVE;
See Also
--------
""" # noqa: E501

def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
file_space = get_file_space(params)

file_type = params['file_type']
if not file_type:
raise KeyError('file type was not specified')

file_type = file_type.lower()
if file_type not in ['file', 'folder']:
raise ValueError('file type must be either FILE or FOLDER')

if file_type == 'file':
file_space.remove(params['path'])
elif file_type == 'folder':
if params['recursive']:
file_space.removedirs(params['path'])
else:
file_space.rmdir(params['path'])

return None


DropHandler.register(overwrite=True)


class CreateFolderHandler(SQLHandler):
"""
CREATE <file-location> FOLDER path
[ overwrite ];
# Path to folder
path = '<path>'
# Should an existing folder be overwritten?
overwrite = OVERWRITE
Description
-----------
Creates a new folder at the specified path in a personal/shared space.
Arguments
---------
* ``<file-location>``: The location of the file, it can
be either 'PERSONAL' or 'SHARED'.
* ``<path>``: The path in a personal/shared space where the folder
is created. The path must end with a trailing slash (/).
Remarks
-------
* If the ``OVERWRITE`` clause is specified, any existing
folder at the specified path is overwritten.
Example
-------
The following command creates a folder in a personal/shared space::
CREATE PERSONAL FOLDER `/data/csv/`;
CREATE SHARED FOLDER `/data/csv/`;
"""

def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
file_space = get_file_space(params)
file_space.mkdir(params['path'], overwrite=params['overwrite'])
return None


CreateFolderHandler.register(overwrite=True)
Loading

0 comments on commit c33007a

Please sign in to comment.