fileglancer 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. fileglancer/__init__.py +32 -0
  2. fileglancer/_version.py +4 -0
  3. fileglancer/app.py +93 -0
  4. fileglancer/filestore.py +278 -0
  5. fileglancer/handlers.py +490 -0
  6. fileglancer/labextension/build_log.json +732 -0
  7. fileglancer/labextension/package.json +118 -0
  8. fileglancer/labextension/static/lib_src_index_js.a6a778b640dccfd3c15f.js +70 -0
  9. fileglancer/labextension/static/lib_src_index_js.a6a778b640dccfd3c15f.js.map +1 -0
  10. fileglancer/labextension/static/remoteEntry.98df9ae205e55c14abe8.js +551 -0
  11. fileglancer/labextension/static/remoteEntry.98df9ae205e55c14abe8.js.map +1 -0
  12. fileglancer/labextension/static/style.js +4 -0
  13. fileglancer/labextension/static/style_index_css.46b2a49d8245bc7a4b7c.js +471 -0
  14. fileglancer/labextension/static/style_index_css.46b2a49d8245bc7a4b7c.js.map +1 -0
  15. fileglancer/paths.py +152 -0
  16. fileglancer/preferences.py +251 -0
  17. fileglancer/tests/__init__.py +1 -0
  18. fileglancer/tests/test_filestore.py +171 -0
  19. fileglancer/tests/test_handlers.py +49 -0
  20. fileglancer/tests/test_mock_server.py +63 -0
  21. fileglancer/ui/assets/blosc-E49GQuAK.js +18 -0
  22. fileglancer/ui/assets/blosc-E49GQuAK.js.map +1 -0
  23. fileglancer/ui/assets/chunk-INHXZS53-D3tQiqtZ.js +2 -0
  24. fileglancer/ui/assets/chunk-INHXZS53-D3tQiqtZ.js.map +1 -0
  25. fileglancer/ui/assets/index-CpLWWuWv.css +1 -0
  26. fileglancer/ui/assets/index-DvuWJ9tx.js +96 -0
  27. fileglancer/ui/assets/index-DvuWJ9tx.js.map +1 -0
  28. fileglancer/ui/assets/lz4-BIGKWw27.js +16 -0
  29. fileglancer/ui/assets/lz4-BIGKWw27.js.map +1 -0
  30. fileglancer/ui/assets/zstd-IvP746pw.js +16 -0
  31. fileglancer/ui/assets/zstd-IvP746pw.js.map +1 -0
  32. fileglancer/ui/index.html +14 -0
  33. fileglancer/ui/logo.svg +101 -0
  34. fileglancer-0.2.0.data/data/etc/jupyter/jupyter_server_config.d/fileglancer.json +7 -0
  35. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/build_log.json +732 -0
  36. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/install.json +5 -0
  37. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/package.json +118 -0
  38. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/static/lib_src_index_js.a6a778b640dccfd3c15f.js +70 -0
  39. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/static/lib_src_index_js.a6a778b640dccfd3c15f.js.map +1 -0
  40. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/static/remoteEntry.98df9ae205e55c14abe8.js +551 -0
  41. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/static/remoteEntry.98df9ae205e55c14abe8.js.map +1 -0
  42. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/static/style.js +4 -0
  43. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/static/style_index_css.46b2a49d8245bc7a4b7c.js +471 -0
  44. fileglancer-0.2.0.data/data/share/jupyter/labextensions/fileglancer/static/style_index_css.46b2a49d8245bc7a4b7c.js.map +1 -0
  45. fileglancer-0.2.0.dist-info/METADATA +177 -0
  46. fileglancer-0.2.0.dist-info/RECORD +48 -0
  47. fileglancer-0.2.0.dist-info/WHEEL +4 -0
  48. fileglancer-0.2.0.dist-info/licenses/LICENSE +29 -0
@@ -0,0 +1,32 @@
1
+ """Initialize the backend server extension"""
2
+
3
+ try:
4
+ from ._version import __version__
5
+ except ImportError:
6
+ # Fallback when using the package in dev mode without installing
7
+ # in editable mode with pip. It is highly recommended to install
8
+ # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs
9
+ import warnings
10
+ warnings.warn("Importing 'fileglancer' outside a proper installation.")
11
+ __version__ = "dev"
12
+
13
+ import os
14
+ from fileglancer.app import Fileglancer
15
+
16
+ def _jupyter_labextension_paths():
17
+ return [{
18
+ "src": "labextension",
19
+ "dest": "fileglancer"
20
+ }]
21
+
22
+ def _jupyter_server_extension_points():
23
+ """
24
+ Returns a list of dictionaries with metadata describing
25
+ where to find the `_load_jupyter_server_extension` function.
26
+ """
27
+ return [
28
+ {
29
+ "module": "fileglancer.app",
30
+ "app": Fileglancer
31
+ }
32
+ ]
@@ -0,0 +1,4 @@
1
+ # This file is auto-generated by Hatchling. As such, do not:
2
+ # - modify
3
+ # - track in version control e.g. be sure to add to .gitignore
4
+ __version__ = VERSION = '0.2.0'
fileglancer/app.py ADDED
@@ -0,0 +1,93 @@
1
+ from jupyter_server.extension.application import ExtensionApp
2
+ from fileglancer.handlers import (
3
+ FileSharePathsHandler,
4
+ FileShareHandler,
5
+ VersionHandler,
6
+ StaticHandler,
7
+ PreferencesHandler,
8
+ TicketHandler,
9
+ )
10
+ from pathlib import Path, PurePath
11
+ from traitlets import (
12
+ TraitType,
13
+ Undefined,
14
+ Unicode,
15
+ Bool,
16
+ )
17
+
18
+ class PathType(TraitType):
19
+ """A pathlib traitlet type which allows string and undefined values."""
20
+
21
+ @property
22
+ def info_text(self):
23
+ return 'a pathlib.PurePath object'
24
+
25
+ def validate(self, obj, value):
26
+ if isinstance(value, str):
27
+ return Path(value).expanduser()
28
+ if isinstance(value, PurePath):
29
+ return value
30
+ if value == Undefined:
31
+ return value
32
+ self.error(obj, value)
33
+
34
+
35
+ class Fileglancer(ExtensionApp):
36
+
37
+ name = "fileglancer"
38
+ app_name = "fileglancer-server"
39
+ load_other_extensions = True
40
+ default_url = "/fg/"
41
+
42
+ ui_path = PathType(
43
+ default_value=Path(__file__).parent / "ui",
44
+ config=False,
45
+ help="Path to the UI files.",
46
+ )
47
+
48
+ central_url = Unicode(
49
+ None,
50
+ allow_none=True,
51
+ config=True,
52
+ help="The URL of the central server",
53
+ )
54
+
55
+ dev_mode = Bool(
56
+ default_value=False,
57
+ config=True,
58
+ help="Enable development mode.",
59
+ )
60
+
61
+ def initialize_settings(self):
62
+ """Update extension settings.
63
+
64
+ Update the self.settings trait to pass extra settings to the underlying
65
+ Tornado Web Application.
66
+
67
+ self.settings.update({'<trait>':...})
68
+ """
69
+ super().initialize_settings()
70
+
71
+ # startup messages
72
+ self.log.info("Starting Fileglancer server...")
73
+ self.log.info(f'Serving UI from: {self.ui_path}')
74
+ self.log.debug(
75
+ 'FileGlancerServer config:\n' + '\n'.join(
76
+ f' * {key} = {repr(value)}'
77
+ for key, value in self.config['Fileglancer'].items()
78
+ )
79
+ )
80
+
81
+ def initialize_handlers(self):
82
+ self.handlers.extend([
83
+ (r"/api/fileglancer/file-share-paths", FileSharePathsHandler),
84
+ (r"/api/fileglancer/files/(.*)", FileShareHandler),
85
+ (r"/api/fileglancer/files", FileShareHandler),
86
+ (r"/api/fileglancer/version", VersionHandler),
87
+ (r"/api/fileglancer/preference", PreferencesHandler),
88
+ (r"/api/fileglancer/ticket", TicketHandler),
89
+ (r"/fg/(.*)", StaticHandler, {
90
+ "path": str(self.ui_path),
91
+ "default_filename": "index.html",
92
+ }),
93
+ ])
@@ -0,0 +1,278 @@
1
+ """
2
+ A module that provides a simple interface for interacting with a file system,
3
+ rooted at a specific directory.
4
+ """
5
+
6
+ import os
7
+
8
+ from pydantic import BaseModel
9
+ from typing import Optional, Generator
10
+ import stat
11
+ import pwd
12
+ import grp
13
+ import logging
14
+ import shutil
15
+
16
+ from fileglancer.paths import FileSharePath
17
+
18
+ log = logging.getLogger("tornado.application")
19
+
20
+ DEFAULT_BUFFER_SIZE = 8192
21
+
22
+ class FileInfo(BaseModel):
23
+ """
24
+ A class that represents a file or directory in a Filestore.
25
+ """
26
+ name: str
27
+ path: Optional[str] = None
28
+ size: int
29
+ is_dir: bool
30
+ permissions: str
31
+ owner: Optional[str] = None
32
+ group: Optional[str] = None
33
+ last_modified: Optional[float] = None
34
+
35
+ @classmethod
36
+ def from_stat(cls, path: str, full_path: str, stat_result: os.stat_result):
37
+ """Create FileInfo from os.stat_result"""
38
+ is_dir = stat.S_ISDIR(stat_result.st_mode)
39
+ size = 0 if is_dir else stat_result.st_size
40
+ name = os.path.basename(full_path)
41
+ permissions = stat.filemode(stat_result.st_mode)
42
+ last_modified = stat_result.st_mtime
43
+
44
+ try:
45
+ owner = pwd.getpwuid(stat_result.st_uid).pw_name
46
+ except KeyError:
47
+ # If the user ID is not found, use the user ID as the owner
48
+ owner = str(stat_result.st_uid)
49
+
50
+ try:
51
+ group = grp.getgrgid(stat_result.st_gid).gr_name
52
+ except KeyError:
53
+ # If the group ID is not found, use the group ID as the group
54
+ group = str(stat_result.st_gid)
55
+
56
+ return cls(
57
+ name=name,
58
+ path=path,
59
+ size=size,
60
+ is_dir=is_dir,
61
+ permissions=permissions,
62
+ owner=owner,
63
+ group=group,
64
+ last_modified=last_modified
65
+ )
66
+
67
+
68
+ class Filestore:
69
+ """
70
+ A class that provides a simple interface for interacting with a file system,
71
+ rooted at a specific directory.
72
+ """
73
+
74
+ def __init__(self, file_share_path: FileSharePath):
75
+ """
76
+ Create a Filestore with the given root path.
77
+ """
78
+ self.root_path = os.path.abspath(file_share_path.mount_path)
79
+
80
+
81
+ def _check_path_in_root(self, path: str) -> str:
82
+ """
83
+ Check if a path is within the root directory and return the full path.
84
+
85
+ Args:
86
+ path (str): The relative path to check.
87
+
88
+ Returns:
89
+ str: The full path to the file or directory.
90
+
91
+ Raises:
92
+ ValueError: If path attempts to escape root directory
93
+ """
94
+ if path is None or path == "":
95
+ full_path = self.root_path
96
+ else:
97
+ full_path = os.path.abspath(os.path.join(self.root_path, path))
98
+ if not full_path.startswith(self.root_path):
99
+ raise ValueError(f"Path ({full_path}) attempts to escape root directory ({self.root_path})")
100
+ return full_path
101
+
102
+
103
+ def get_root_path(self) -> str:
104
+ """
105
+ Get the root path of the Filestore.
106
+ """
107
+ return self.root_path
108
+
109
+
110
+ def get_file_info(self, path: Optional[str] = None) -> FileInfo:
111
+ """
112
+ Get the FileInfo for a file or directory at the given path.
113
+
114
+ Args:
115
+ path (str): The relative path to the file or directory to get the FileInfo for.
116
+ May be None, in which case the root directory is used.
117
+
118
+ Raises:
119
+ ValueError: If path attempts to escape root directory
120
+ """
121
+ full_path = self._check_path_in_root(path)
122
+ stat_result = os.stat(full_path)
123
+ return FileInfo.from_stat(path, full_path,stat_result)
124
+
125
+
126
+ def yield_file_infos(self, path: Optional[str] = None) -> Generator[FileInfo, None, None]:
127
+ """
128
+ Yield a FileInfo object for each child of the given path.
129
+
130
+ Args:
131
+ path (str): The relative path to the directory to list.
132
+ May be None, in which case the root directory is listed.
133
+
134
+ Raises:
135
+ ValueError: If path attempts to escape root directory
136
+ """
137
+ full_path = self._check_path_in_root(path)
138
+ try:
139
+ for entry in os.listdir(full_path):
140
+ entry_path = os.path.join(full_path, entry)
141
+ try:
142
+ stat_result = os.stat(entry_path)
143
+ rel_entry_path = os.path.relpath(entry_path, self.root_path)
144
+ file_info = FileInfo.from_stat(rel_entry_path, entry_path, stat_result)
145
+ yield file_info
146
+ except (FileNotFoundError, PermissionError):
147
+ continue
148
+ except (FileNotFoundError, PermissionError):
149
+ return
150
+
151
+
152
+ def stream_file_contents(self, path: str, buffer_size: int = DEFAULT_BUFFER_SIZE) -> Generator[bytes, None, None]:
153
+ """
154
+ Stream the contents of a file at the given path.
155
+
156
+ Args:
157
+ path (str): The path to the file to stream.
158
+ buffer_size (int): The size of the buffer to use when reading the file.
159
+ Defaults to DEFAULT_BUFFER_SIZE, which is 8192 bytes.
160
+
161
+ Raises:
162
+ ValueError: If path attempts to escape root directory
163
+ """
164
+ if path is None or path == "":
165
+ raise ValueError("Path cannot be None or empty")
166
+ full_path = self._check_path_in_root(path)
167
+ with open(full_path, 'rb') as file:
168
+ while True:
169
+ chunk = file.read(buffer_size)
170
+ if not chunk:
171
+ break
172
+ yield chunk
173
+
174
+
175
+ def rename_file_or_dir(self, old_path: str, new_path: str):
176
+ """
177
+ Rename a file at the given old path to the new path.
178
+
179
+ Args:
180
+ old_path (str): The relative path to the file to rename.
181
+ new_path (str): The new relative path for the file.
182
+
183
+ Raises:
184
+ ValueError: If either path attempts to escape root directory
185
+ """
186
+ if old_path is None or old_path == "":
187
+ raise ValueError("Old path cannot be None or empty")
188
+ if new_path is None or new_path == "":
189
+ raise ValueError("New path cannot be None or empty")
190
+ full_old_path = self._check_path_in_root(old_path)
191
+ full_new_path = self._check_path_in_root(new_path)
192
+ os.rename(full_old_path, full_new_path)
193
+
194
+
195
+ def remove_file_or_dir(self, path: str):
196
+ """
197
+ Delete a file or (empty) directory at the given path.
198
+
199
+ Args:
200
+ path (str): The relative path to the file to delete.
201
+
202
+ Raises:
203
+ ValueError: If path is None or empty, or attempts to escape root directory
204
+ """
205
+ if path is None or path == "":
206
+ raise ValueError("Path cannot be None or empty")
207
+ full_path = self._check_path_in_root(path)
208
+ if os.path.isdir(full_path):
209
+ shutil.rmtree(full_path)
210
+ else:
211
+ os.remove(full_path)
212
+
213
+
214
+ def create_dir(self, path: str):
215
+ """
216
+ Create a directory at the given path.
217
+
218
+ Args:
219
+ path (str): The relative path to the directory to create.
220
+
221
+ Raises:
222
+ ValueError: If path is None or empty, or attempts to escape root directory
223
+ """
224
+ if path is None or path == "":
225
+ raise ValueError("Path cannot be None or empty")
226
+ full_path = self._check_path_in_root(path)
227
+ os.mkdir(full_path)
228
+
229
+
230
+ def create_empty_file(self, path: str):
231
+ """
232
+ Create an empty file at the given path.
233
+
234
+ Args:
235
+ path (str): The relative path to the file to create.
236
+
237
+ Raises:
238
+ ValueError: If path is None or empty, or attempts to escape root directory
239
+ """
240
+ if path is None or path == "":
241
+ raise ValueError("Path cannot be None or empty")
242
+ full_path = self._check_path_in_root(path)
243
+ open(full_path, 'w').close()
244
+
245
+
246
+ def change_file_permissions(self, path: str, permissions: str):
247
+ """
248
+ Change the permissions of a file at the given path.
249
+
250
+ Args:
251
+ path (str): The relative path to the file to change the permissions of.
252
+ permissions (str): The new permissions to set for the file.
253
+ Must be a string of length 10, like '-rw-r--r--'.
254
+
255
+ Raises:
256
+ ValueError: If path is None or empty, or attempts to escape root directory,
257
+ or permissions is not a string of length 10.
258
+ """
259
+ if path is None or path == "":
260
+ raise ValueError("Path cannot be None or empty")
261
+ if len(permissions) != 10:
262
+ raise ValueError("Permissions must be a string of length 10")
263
+ full_path = self._check_path_in_root(path)
264
+ # Convert permission string (like '-rw-r--r--') to octal mode
265
+ mode = 0
266
+ # Owner permissions (positions 1-3)
267
+ if permissions[1] == 'r': mode |= stat.S_IRUSR
268
+ if permissions[2] == 'w': mode |= stat.S_IWUSR
269
+ if permissions[3] == 'x': mode |= stat.S_IXUSR
270
+ # Group permissions (positions 4-6)
271
+ if permissions[4] == 'r': mode |= stat.S_IRGRP
272
+ if permissions[5] == 'w': mode |= stat.S_IWGRP
273
+ if permissions[6] == 'x': mode |= stat.S_IXGRP
274
+ # Other permissions (positions 7-9)
275
+ if permissions[7] == 'r': mode |= stat.S_IROTH
276
+ if permissions[8] == 'w': mode |= stat.S_IWOTH
277
+ if permissions[9] == 'x': mode |= stat.S_IXOTH
278
+ os.chmod(full_path, mode)