Skip to content

Commit ac1f030

Browse files
feat(fusion-sql): add support for Files API (personal and shared spaces)
1 parent 5b2c28e commit ac1f030

File tree

3 files changed

+423
-0
lines changed

3 files changed

+423
-0
lines changed

singlestoredb/fusion/handler.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@
7474
'<column>': '',
7575
'<catalog-name>': '',
7676
'<link-name>': '',
77+
'<file-location>': r'''
78+
file_location = { PERSONAL | SHARED }
79+
''',
80+
'<file-type>': r'''
81+
file_type = { FILE | FOLDER }
82+
''',
7783
}
7884

7985
BUILTIN_DEFAULTS = { # type: ignore
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
#!/usr/bin/env python3
2+
from typing import Any
3+
from typing import Dict
4+
from typing import Optional
5+
6+
from .. import result
7+
from ..handler import SQLHandler
8+
from ..result import FusionSQLResult
9+
from .utils import dt_isoformat
10+
from .utils import get_file_space
11+
12+
13+
class ShowFilesHandler(SQLHandler):
14+
"""
15+
SHOW <file-location> FILES
16+
[ at_path ] [ <like> ]
17+
[ <order-by> ]
18+
[ <limit> ] [ recursive ] [ extended ];
19+
20+
# File path to list
21+
at_path = AT '<path>'
22+
23+
# Should the listing be recursive?
24+
recursive = RECURSIVE
25+
26+
# Should extended attributes be shown?
27+
extended = EXTENDED
28+
29+
Description
30+
-----------
31+
Displays a list of files in a personal/shared space.
32+
33+
Arguments
34+
---------
35+
* ``<file-location>``: The location of the file, it can
36+
be either 'PERSONAL' or 'SHARED'.
37+
* ``<path>``: A path in the personal/shared space.
38+
* ``<pattern>``: A pattern similar to SQL LIKE clause.
39+
Uses ``%`` as the wildcard character.
40+
41+
Remarks
42+
-------
43+
* Use the ``LIKE`` clause to specify a pattern and return only the
44+
files that match the specified pattern.
45+
* The ``LIMIT`` clause limits the number of results to the
46+
specified number.
47+
* Use the ``ORDER BY`` clause to sort the results by the specified
48+
key. By default, the results are sorted in the ascending order.
49+
* The ``AT PATH`` clause specifies the path in the personal/shared
50+
space to list the files from.
51+
* Use the ``RECURSIVE`` clause to list the files recursively.
52+
* To return more information about the files, use the ``EXTENDED``
53+
clause.
54+
55+
Examples
56+
--------
57+
The following commands list the files at a specific path::
58+
59+
SHOW PERSONAL FILES AT PATH "/data/";
60+
SHOW SHARED FILES AT PATH "/data/";
61+
62+
The following commands list the files recursively with
63+
additional information::
64+
65+
SHOW PERSONAL FILES RECURSIVE EXTENDED;
66+
SHOW SHARED FILES RECURSIVE EXTENDED;
67+
68+
See Also
69+
--------
70+
* ``UPLOAD PERSONAL FILE``
71+
* ``UPLOAD SHARED FILE``
72+
* ``DOWNLOAD PERSONAL FILE``
73+
* ``DOWNLOAD SHARED FILE``
74+
75+
""" # noqa: E501
76+
77+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
78+
file_space = get_file_space(params)
79+
80+
res = FusionSQLResult()
81+
res.add_field('Name', result.STRING)
82+
83+
if params['extended']:
84+
res.add_field('Type', result.STRING)
85+
res.add_field('Size', result.INTEGER)
86+
res.add_field('Writable', result.STRING)
87+
res.add_field('CreatedAt', result.DATETIME)
88+
res.add_field('LastModifiedAt', result.DATETIME)
89+
90+
files = []
91+
for x in file_space.listdir(
92+
params['at_path'] or '/',
93+
recursive=params['recursive'],
94+
):
95+
info = file_space.info(x)
96+
files.append(
97+
tuple([
98+
x, info.type, info.size or 0, info.writable,
99+
dt_isoformat(info.created_at),
100+
dt_isoformat(info.last_modified_at),
101+
]),
102+
)
103+
res.set_rows(files)
104+
105+
else:
106+
res.set_rows([(x,) for x in file_space.listdir(
107+
params['at_path'] or '/',
108+
recursive=params['recursive'],
109+
)])
110+
111+
if params['like']:
112+
res = res.like(Name=params['like'])
113+
114+
return res.order_by(**params['order_by']).limit(params['limit'])
115+
116+
117+
ShowFilesHandler.register(overwrite=True)
118+
119+
120+
class UploadFileHandler(SQLHandler):
121+
"""
122+
UPLOAD <file-location> FILE TO path
123+
FROM local_path [ overwrite ];
124+
125+
# Path to file
126+
path = '<path>'
127+
128+
# Path to local file
129+
local_path = '<local-path>'
130+
131+
# Should an existing file be overwritten?
132+
overwrite = OVERWRITE
133+
134+
Description
135+
-----------
136+
Uploads a file to a personal/shared space.
137+
138+
Arguments
139+
---------
140+
* ``<file-location>``: The location of the file, it can
141+
be either 'PERSONAL' or 'SHARED'.
142+
* ``<path>``: The path in the personal/shared space where the file is uploaded.
143+
* ``<local-path>``: The path to the file to upload in the local
144+
directory.
145+
146+
Remarks
147+
-------
148+
* If the ``OVERWRITE`` clause is specified, any existing file at the
149+
specified path in the personal/shared space is overwritten.
150+
151+
Examples
152+
--------
153+
The following commands upload a file to a personal/shared space and overwrite any
154+
existing files at the specified path::
155+
156+
UPLOAD PERSONAL FILE TO '/data/stats.csv'
157+
FROM '/tmp/user/stats.csv' OVERWRITE;
158+
UPLOAD SHARED FILE TO '/data/stats.csv'
159+
FROM '/tmp/user/stats.csv' OVERWRITE;
160+
161+
See Also
162+
--------
163+
* ``DOWNLOAD PERSONAL FILE``
164+
* ``DOWNLOAD SHARED FILE``
165+
166+
""" # noqa: E501
167+
168+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
169+
file_space = get_file_space(params)
170+
file_space.upload_file(
171+
params['local_path'], params['path'],
172+
overwrite=params['overwrite'],
173+
)
174+
return None
175+
176+
177+
UploadFileHandler.register(overwrite=True)
178+
179+
180+
class DownloadFileHandler(SQLHandler):
181+
"""
182+
DOWNLOAD <file-location> FILE path
183+
[ local_path ]
184+
[ overwrite ]
185+
[ encoding ];
186+
187+
# Path to file
188+
path = '<path>'
189+
190+
# Path to local file
191+
local_path = TO '<local-path>'
192+
193+
# Should an existing file be overwritten?
194+
overwrite = OVERWRITE
195+
196+
# File encoding
197+
encoding = ENCODING '<encoding>'
198+
199+
Description
200+
-----------
201+
Download a file from a personal/shared space.
202+
203+
Arguments
204+
---------
205+
* ``<file-location>``: The location of the file, it can
206+
be either 'PERSONAL' or 'SHARED'.
207+
* ``<path>``: The path to the file to download in a personal/shared space.
208+
* ``<encoding>``: The encoding to apply to the downloaded file.
209+
* ``<local-path>``: Specifies the path in the local directory
210+
where the file is downloaded.
211+
212+
Remarks
213+
-------
214+
* If the ``OVERWRITE`` clause is specified, any existing file at
215+
the download location is overwritten.
216+
* By default, files are downloaded in binary encoding. To view
217+
the contents of the file on the standard output, use the
218+
``ENCODING`` clause and specify an encoding.
219+
* If ``<local-path>`` is not specified, the file is displayed
220+
on the standard output.
221+
222+
Examples
223+
--------
224+
The following commands display the contents of the file on the
225+
standard output::
226+
227+
DOWNLOAD PERSONAL FILE '/data/stats.csv' ENCODING 'utf8';
228+
DOWNLOAD SHARED FILE '/data/stats.csv' ENCODING 'utf8';
229+
230+
The following commands download a file to a specific location and
231+
overwrites any existing file with the name ``stats.csv`` on the local storage::
232+
233+
DOWNLOAD PERSONAL FILE '/data/stats.csv'
234+
TO '/tmp/data.csv' OVERWRITE;
235+
DOWNLOAD SHARED FILE '/data/stats.csv'
236+
TO '/tmp/data.csv' OVERWRITE;
237+
238+
See Also
239+
--------
240+
* ``UPLOAD PERSONAL FILE``
241+
* ``UPLOAD SHARED FILE``
242+
243+
""" # noqa: E501
244+
245+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
246+
file_space = get_file_space(params)
247+
248+
out = file_space.download_file(
249+
params['path'],
250+
local_path=params['local_path'] or None,
251+
overwrite=params['overwrite'],
252+
encoding=params['encoding'] or None,
253+
)
254+
255+
if not params['local_path']:
256+
res = FusionSQLResult()
257+
if params['encoding']:
258+
res.add_field('Data', result.STRING)
259+
else:
260+
res.add_field('Data', result.BLOB)
261+
res.set_rows([(out,)])
262+
return res
263+
264+
return None
265+
266+
267+
DownloadFileHandler.register(overwrite=True)
268+
269+
270+
class DropHandler(SQLHandler):
271+
"""
272+
DROP <file-location> <file-type> path
273+
[ recursive ];
274+
275+
# Path to file
276+
path = '<path>'
277+
278+
# Should folders be deleted recursively?
279+
recursive = RECURSIVE
280+
281+
Description
282+
-----------
283+
Deletes a file/folder from a personal/shared space.
284+
285+
Arguments
286+
---------
287+
* ``<file-location>``: The location of the file, it can
288+
be either 'PERSONAL' or 'SHARED'.
289+
* ``<file-type>``: The type of the file, it can
290+
be either 'FILE' or 'FOLDER'.
291+
* ``<path>``: The path to the file to delete in a personal/shared space.
292+
293+
Remarks
294+
-------
295+
* The ``RECURSIVE`` clause indicates that the specified folder
296+
is deleted recursively.
297+
298+
Example
299+
--------
300+
The following commands delete a file/folder from a personal/shared space::
301+
302+
DROP PERSONAL FILE '/data/stats.csv';
303+
DROP SHARED FILE '/data/stats.csv';
304+
DROP PERSONAL FOLDER '/data/' RECURSIVE;
305+
DROP SHARED FOLDER '/data/' RECURSIVE;
306+
307+
See Also
308+
--------
309+
310+
""" # noqa: E501
311+
312+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
313+
file_space = get_file_space(params)
314+
315+
file_type = params['file_type']
316+
if not file_type:
317+
raise KeyError('file type was not specified')
318+
319+
file_type = file_type.lower()
320+
if file_type not in ['file', 'folder']:
321+
raise ValueError('file type must be either FILE or FOLDER')
322+
323+
if file_type == 'file':
324+
file_space.remove(params['path'])
325+
elif file_type == 'folder':
326+
if params['recursive']:
327+
file_space.removedirs(params['path'])
328+
else:
329+
file_space.rmdir(params['path'])
330+
331+
return None
332+
333+
334+
DropHandler.register(overwrite=True)
335+
336+
337+
class CreateFolderHandler(SQLHandler):
338+
"""
339+
CREATE <file-location> FOLDER path
340+
[ overwrite ];
341+
342+
# Path to folder
343+
path = '<path>'
344+
345+
# Should an existing folder be overwritten?
346+
overwrite = OVERWRITE
347+
348+
Description
349+
-----------
350+
Creates a new folder at the specified path in a personal/shared space.
351+
352+
Arguments
353+
---------
354+
* ``<file-location>``: The location of the file, it can
355+
be either 'PERSONAL' or 'SHARED'.
356+
* ``<path>``: The path in a personal/shared space where the folder
357+
is created. The path must end with a trailing slash (/).
358+
359+
Remarks
360+
-------
361+
* If the ``OVERWRITE`` clause is specified, any existing
362+
folder at the specified path is overwritten.
363+
364+
Example
365+
-------
366+
The following command creates a folder in a personal/shared space::
367+
368+
CREATE PERSONAL FOLDER `/data/csv/`;
369+
CREATE SHARED FOLDER `/data/csv/`;
370+
371+
"""
372+
373+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
374+
file_space = get_file_space(params)
375+
file_space.mkdir(params['path'], overwrite=params['overwrite'])
376+
return None
377+
378+
379+
CreateFolderHandler.register(overwrite=True)

0 commit comments

Comments
 (0)