|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import datetime |
| 6 | +import errno |
6 | 7 | import glob |
7 | 8 | import io |
8 | 9 | import os |
9 | 10 | import re |
10 | 11 | from abc import ABC |
11 | 12 | from abc import abstractmethod |
| 13 | +import stat |
| 14 | +import sys |
| 15 | +import time |
12 | 16 | from typing import Any |
13 | 17 | from typing import BinaryIO |
14 | 18 | from typing import Dict |
|
17 | 21 | from typing import TextIO |
18 | 22 | from typing import Union |
19 | 23 |
|
| 24 | +import pyfuse3 |
| 25 | +import trio |
| 26 | + |
20 | 27 | from .. import config |
21 | 28 | from ..exceptions import ManagementError |
22 | 29 | from .manager import Manager |
@@ -311,7 +318,7 @@ def getctime(self) -> float: |
311 | 318 | if self.created_at is None: |
312 | 319 | return 0.0 |
313 | 320 | return self.created_at.timestamp() |
314 | | - |
| 321 | + |
315 | 322 |
|
316 | 323 | class FilesObjectTextWriter(io.StringIO): |
317 | 324 | """StringIO wrapper for writing to FileLocation.""" |
@@ -349,6 +356,208 @@ class FilesObjectBytesReader(io.BytesIO): |
349 | 356 | """BytesIO wrapper for reading from FileLocation.""" |
350 | 357 |
|
351 | 358 |
|
| 359 | +class Inode: |
| 360 | + def __init__(self, fs, parent, name, id=None): |
| 361 | + self.fs = fs |
| 362 | + |
| 363 | + if id is None: |
| 364 | + id = fs.next_id |
| 365 | + fs.next_id += 1 |
| 366 | + |
| 367 | + self.name = name |
| 368 | + self.parent = parent |
| 369 | + self.id = id |
| 370 | + self._children = None |
| 371 | + |
| 372 | + def __repr__(self): |
| 373 | + return f"Inode({self.id}, \"{self.name}\", {self._children})" |
| 374 | + |
| 375 | + def getNameWithoutTrailingSlash(self): |
| 376 | + if self.name == "": |
| 377 | + return self.name |
| 378 | + if self.isDir(): |
| 379 | + return self.name[:-1] |
| 380 | + return self.name |
| 381 | + |
| 382 | + def getStagePath(self): |
| 383 | + if self.parent is None: |
| 384 | + return self.name |
| 385 | + return self.parent.getStagePath() + self.name |
| 386 | + |
| 387 | + def isDir(self): |
| 388 | + return self.name == "" or self.name.endswith("/") |
| 389 | + |
| 390 | + def isFile(self): |
| 391 | + return not self.isDir() |
| 392 | + |
| 393 | + def children(self): |
| 394 | + if self._children is None: |
| 395 | + self._children = [] |
| 396 | + childrenNames = self.fs.stage.listdir(self.getStagePath()) |
| 397 | + for childName in childrenNames: |
| 398 | + childInode = Inode(self.fs, self, childName) |
| 399 | + self.fs.inodes[childInode.id] = childInode |
| 400 | + self._children.append(childInode.id) |
| 401 | + |
| 402 | + return self._children |
| 403 | + |
| 404 | +class SinglestoreFS(pyfuse3.Operations): |
| 405 | + def __init__(self, fileLocation: FileLocation): |
| 406 | + super(SinglestoreFS, self).__init__() |
| 407 | + self.next_id = pyfuse3.ROOT_INODE + 1 |
| 408 | + |
| 409 | + """ |
| 410 | + How to use: |
| 411 | + workspaceManager = s2.manage_workspaces(access_token, base_url=base_url) |
| 412 | + workspaceGroup = workspaceManager.get_workspace_group(workspace_group) |
| 413 | + return workspaceGroup.stage |
| 414 | + """ |
| 415 | + |
| 416 | + self.stage = fileLocation |
| 417 | + |
| 418 | + self.inodes = { |
| 419 | + pyfuse3.ROOT_INODE: Inode(self, None, "", pyfuse3.ROOT_INODE), |
| 420 | + } |
| 421 | + |
| 422 | + async def getattr(self, id, ctx=None): |
| 423 | + inode = self.inodes[id] |
| 424 | + info = self.stage.info(inode.getStagePath()) |
| 425 | + |
| 426 | + entry = pyfuse3.EntryAttributes() |
| 427 | + if inode.isDir(): |
| 428 | + entry.st_mode = (stat.S_IFDIR | 0o555) |
| 429 | + entry.st_size = 0 |
| 430 | + elif inode.isFile(): |
| 431 | + entry.st_mode = (stat.S_IFREG | 0o555) |
| 432 | + entry.st_size = info.size |
| 433 | + else: |
| 434 | + raise pyfuse3.FUSEError(errno.ENOENT) |
| 435 | + if info.writable: |
| 436 | + entry.st_mode |= 0o222 |
| 437 | + |
| 438 | + entry.st_atime_ns = time.time_ns() # Current timestamp |
| 439 | + entry.st_ctime_ns = 0 |
| 440 | + if info.created_at is not None: |
| 441 | + entry.st_ctime_ns = info.created_at.timestamp()*1e9 |
| 442 | + entry.st_mtime_ns = 0 |
| 443 | + if info.last_modified_at is not None: |
| 444 | + entry.st_mtime_ns = info.last_modified_at.timestamp()*1e9 |
| 445 | + entry.st_gid = os.getgid() # TODO: check |
| 446 | + entry.st_uid = os.getuid() # TODO: check |
| 447 | + entry.st_ino = id |
| 448 | + |
| 449 | + return entry |
| 450 | + |
| 451 | + async def lookup(self, parent_id, name, ctx=None): |
| 452 | + parent_inode = self.inodes[parent_id] |
| 453 | + |
| 454 | + assert parent_inode.isDir() |
| 455 | + |
| 456 | + for child_id in parent_inode.children(): |
| 457 | + child_inode = self.inodes[child_id] |
| 458 | + if child_inode.getNameWithoutTrailingSlash() == name.decode(): |
| 459 | + return await self.getattr(child_id) |
| 460 | + raise pyfuse3.FUSEError(errno.ENOENT) |
| 461 | + |
| 462 | + async def opendir(self, id, ctx): |
| 463 | + if not id in self.inodes: |
| 464 | + raise pyfuse3.FUSEError(errno.ENOENT) |
| 465 | + return id |
| 466 | + |
| 467 | + async def readdir(self, fh, start_id, token): |
| 468 | + if not fh in self.inodes: |
| 469 | + raise pyfuse3.FUSEError(errno.ENOENT) |
| 470 | + |
| 471 | + inode = self.inodes[fh] |
| 472 | + |
| 473 | + assert inode.isDir() |
| 474 | + |
| 475 | + children = {child: self.inodes[child] for child in inode.children() if child > start_id} |
| 476 | + |
| 477 | + for child in children.values(): |
| 478 | + pyfuse3.readdir_reply( |
| 479 | + token, |
| 480 | + child.getNameWithoutTrailingSlash().encode(), |
| 481 | + await self.getattr(child.id), |
| 482 | + child.id |
| 483 | + ) |
| 484 | + return |
| 485 | + |
| 486 | + async def open(self, id, flags, ctx): |
| 487 | + return pyfuse3.FileInfo(fh=id) |
| 488 | + |
| 489 | + async def read(self, fh, off, size): |
| 490 | + inode = self.inodes[fh] |
| 491 | + assert inode.isFile() |
| 492 | + fileContent = self.stage.download_file(inode.getStagePath()) |
| 493 | + return fileContent[off:off+size] |
| 494 | + |
| 495 | + async def create(self, parent_id, name, mode, flags, ctx): |
| 496 | + parent_inode = self.inodes[parent_id] |
| 497 | + assert parent_inode.isDir() |
| 498 | + stagePath = parent_inode.getStagePath() + name.decode() |
| 499 | + self.stage.open(stagePath, "w").close() |
| 500 | + inode = Inode(self, parent_inode, name.decode()) |
| 501 | + self.inodes[inode.id] = inode |
| 502 | + self.inodes[parent_id]._children.append(inode.id) |
| 503 | + return pyfuse3.FileInfo(fh=inode.id), await self.getattr(inode.id) |
| 504 | + |
| 505 | + async def setattr(self, id, attr, fields, fh, ctx): |
| 506 | + return await self.getattr(id) |
| 507 | + |
| 508 | + async def write(self, fh, offset, data): |
| 509 | + inode = self.inodes[fh] |
| 510 | + assert inode.isFile() |
| 511 | + fileContent = self.stage.download_file(inode.getStagePath()) |
| 512 | + newFileContent = fileContent[:offset] + data + fileContent[offset+len(data):] |
| 513 | + with self.stage.open(inode.getStagePath(), "wb") as f: |
| 514 | + return f.write(newFileContent) |
| 515 | + |
| 516 | + async def unlink(self, parent_id, name, ctx): |
| 517 | + parent_inode = self.inodes[parent_id] |
| 518 | + attr = await self.lookup(parent_id, name) |
| 519 | + inode = self.inodes[attr.st_ino] |
| 520 | + assert inode.isFile() |
| 521 | + self.stage.remove(inode.getStagePath()) |
| 522 | + parent_inode._children.remove(inode.id) |
| 523 | + del self.inodes[inode.id] |
| 524 | + return |
| 525 | + |
| 526 | + async def mkdir(self, parent_id, name, mode, ctx): |
| 527 | + parent_inode = self.inodes[parent_id] |
| 528 | + assert parent_inode.isDir() |
| 529 | + stagePath = parent_inode.getStagePath() + name.decode() + "/" |
| 530 | + self.stage.mkdir(stagePath) |
| 531 | + inode = Inode(self, parent_inode, name.decode() + "/") |
| 532 | + self.inodes[inode.id] = inode |
| 533 | + parent_inode._children.append(inode.id) |
| 534 | + return await self.getattr(inode.id) |
| 535 | + |
| 536 | + async def rename(self, parent_id, name, newparent_id, newname, ctx): |
| 537 | + parent_inode = self.inodes[parent_id] |
| 538 | + newparent_inode = self.inodes[newparent_id] |
| 539 | + assert parent_inode.isDir() |
| 540 | + assert newparent_inode.isDir() |
| 541 | + attr = await self.lookup(parent_id, name) |
| 542 | + inode = self.inodes[attr.st_ino] |
| 543 | + self.stage.rename(inode.getStagePath(), newparent_inode.getStagePath() + newname.decode()) |
| 544 | + inode.parent = newparent_inode |
| 545 | + inode.name = newname.decode() |
| 546 | + newparent_inode._children.append(inode.id) |
| 547 | + parent_inode._children.remove(inode.id) |
| 548 | + return |
| 549 | + |
| 550 | + async def rmdir(self, parent_id, name, ctx): |
| 551 | + parent_inode = self.inodes[parent_id] |
| 552 | + assert parent_inode.isDir() |
| 553 | + attr = await self.lookup(parent_id, name) |
| 554 | + inode = self.inodes[attr.st_ino] |
| 555 | + assert inode.isDir() |
| 556 | + self.stage.rmdir(inode.getStagePath()) |
| 557 | + parent_inode._children.remove(inode.id) |
| 558 | + del self.inodes[inode.id] |
| 559 | + return |
| 560 | + |
352 | 561 | class FileLocation(ABC): |
353 | 562 | @abstractmethod |
354 | 563 | def open( |
@@ -472,6 +681,20 @@ def __str__(self) -> str: |
472 | 681 | def __repr__(self) -> str: |
473 | 682 | pass |
474 | 683 |
|
| 684 | + def mount(self, mountpoint) -> None: |
| 685 | + """Mount to folder""" |
| 686 | + fs = SinglestoreFS(self) |
| 687 | + fuse_options = set(pyfuse3.default_options) |
| 688 | + # fuse_options.add('fsname=singlestore_fs') |
| 689 | + # fuse_options.add('debug') |
| 690 | + pyfuse3.init(fs, mountpoint, fuse_options) |
| 691 | + |
| 692 | + try: |
| 693 | + trio.run(pyfuse3.main) |
| 694 | + except: |
| 695 | + pyfuse3.close(unmount=True) |
| 696 | + |
| 697 | + # pyfuse3.close() |
475 | 698 |
|
476 | 699 | class FilesManager(Manager): |
477 | 700 | """ |
|
0 commit comments