22import atexit
33import errno
44import os
5- import re
65import stat
76import sys
87import subprocess
1312from cryptography .hazmat .primitives .ciphers import Cipher
1413from cryptography .hazmat .primitives .ciphers .algorithms import ChaCha20
1514
16- CHUNK_SIZE = 4096
15+ CHUNK_SIZE = 64000
1716
18- UUID4_PATTERN = re .compile (r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' , re .IGNORECASE )
19-
20- def is_valid_uuid4 (filename ):
21- """
22- Validates if the given filename follows the UUIDv4 format.
23-
24- :param filename: The name of the file.
25- :return: True if the filename is a valid UUIDv4, otherwise False.
26- """
27- return bool (UUID4_PATTERN .match (filename ))
2817
2918def is_mount_point (path ):
3019 """
@@ -51,21 +40,20 @@ def unmount_if_mounted(path):
5140 subprocess .run (['fusermount' , '-u' , path ])
5241
5342class EphemeralFile :
43+ fd = enc = dec = None
44+ position = 0
45+
5446 def __init__ (self , filesdir , filename = None ):
5547 """
5648 Initializes an ephemeral file with ChaCha20 encryption.
5749 Creates a new random file path and generates a unique encryption key and nonce.
5850
5951 :param filesdir: The directory where the ephemeral file will be stored.
60- :param filenames : Optional filename. If not provided, a UUID4 is used.
52+ :param filename : Optional filename. If not provided, a UUID4 is used.
6153 """
6254 filename = filename or str (uuid .uuid4 ()) # If filenames is None, generate a random UUID as a string
6355 self .filepath = os .path .join (filesdir , filename )
64- self .cipher = Cipher (ChaCha20 (os .urandom (32 ), uuid .UUID (filename ).bytes [:16 ]), mode = None )
65- self .enc = self .cipher .encryptor ()
66- self .dec = self .cipher .decryptor ()
67-
68- self .fd = None
56+ self .cipher = Cipher (ChaCha20 (os .urandom (32 ), os .urandom (16 )), mode = None )
6957
7058 def __getattribute__ (self , name ):
7159 """
@@ -87,15 +75,24 @@ def __getattribute__(self, name):
8775 # For everything else, defer to the default behavior
8876 return super ().__getattribute__ (name )
8977
90- def open (self , flags , mode = 0o660 ):
78+ def open (self , mode = 'r' ):
9179 """
9280 Opens the ephemeral file for reading or writing.
9381
9482 :param mode: 'w' for writing, 'r' for reading.
9583 :return: The file object.
9684 """
97- self .fd = os .open (self .filepath , os .O_RDWR | os .O_CREAT | os .O_APPEND , mode )
98- os .chmod (self .filepath , mode )
85+ if not self .fd :
86+ self .fd = os .open (self .filepath , os .O_RDWR | os .O_CREAT | os .O_APPEND )
87+
88+ self .enc = self .cipher .encryptor ()
89+ self .dec = self .cipher .decryptor ()
90+
91+ if mode == 'r' :
92+ self .seek (0 )
93+ else :
94+ self .seek (self .size )
95+
9996 return self
10097
10198 def write (self , data ):
@@ -105,6 +102,7 @@ def write(self, data):
105102 :param data: Data to write to the file, can be a string or bytes.
106103 """
107104 os .write (self .fd , self .enc .update (data ))
105+ self .position += len (data )
108106
109107 def read (self , size = None ):
110108 """
@@ -125,7 +123,9 @@ def read(self, size=None):
125123 break
126124
127125 data += self .dec .update (chunk )
128- bytes_read += len (chunk )
126+ chunk_length = len (chunk )
127+ bytes_read += chunk_length
128+ self .position += chunk_length
129129
130130 if size is not None and bytes_read >= size :
131131 break
@@ -138,32 +138,40 @@ def seek(self, offset):
138138
139139 :param offset: The offset to seek to.
140140 """
141- position = 0
142- self .dec = self .cipher .decryptor ()
143- self .enc = self .cipher .encryptor ()
144- os .lseek (self .fd , 0 , os .SEEK_SET )
145- discard_size = offset - position
141+ if offset < self .position :
142+ self .position = 0
143+ self .dec = self .cipher .decryptor ()
144+ self .enc = self .cipher .encryptor ()
145+ os .lseek (self .fd , 0 , os .SEEK_SET )
146+ discard_size = offset
147+ else :
148+ discard_size = offset - self .position
149+
146150 while discard_size > 0 :
147151 to_read = min (CHUNK_SIZE , discard_size )
148152 data = self .dec .update (os .read (self .fd , to_read ))
149153 data = self .enc .update (data )
150154 discard_size -= to_read
151155
156+ self .position = offset
157+
152158 def tell (self ):
153159 """
154160 Returns the current position in the file.
155161
156162 :return: The current position in the file.
157163 """
158- return os . lseek ( self .fd , 0 , os . SEEK_CUR )
164+ return self .position
159165
160166 def close (self ):
161167 """
162168 Closes the file descriptor.
163169 """
164- if self .fd is not None :
170+ if self .fd :
165171 os .close (self .fd )
166- self .fd = None
172+ del self .enc
173+ del self .dec
174+ self .fd = self .enc = self .dec = None
167175
168176 def __enter__ (self ):
169177 """
@@ -197,9 +205,28 @@ def __init__(self, storage_directory=None):
197205 """
198206 self .storage_directory = storage_directory if storage_directory is not None else mkdtemp ()
199207 self .files = {} # Track open files and their secure temporary file handlers
208+ self .directories = {'/' : set ()}
200209 self .mutex = threading .Lock ()
201210
211+ def _split_path (self , path ):
212+ """
213+ Splits a given path into its parent directory and final component.
214+
215+ :param path: The full file or directory path.
216+ :return: A tuple (parent_path, name), where parent_path is the directory
217+ and name is the last part of the path.
218+ """
219+ parts = path .strip ('/' ).split ('/' )
220+ return '/' + '/' .join (parts [:- 1 ]) if len (parts ) > 1 else '/' , parts [- 1 ]
221+
202222 def get_file (self , path ):
223+ """
224+ Retrieves the file object associated with the specified path.
225+
226+ :param path: The full path to the file.
227+ :return: The file object stored at the given path.
228+ :raises FuseOSError: If the file does not exist.
229+ """
203230 file = self .files .get (path )
204231 if file is None :
205232 raise FuseOSError (errno .ENOENT )
@@ -214,20 +241,17 @@ def getattr(self, path, fh=None):
214241 :return: A dictionary of file attributes.
215242 """
216243 with self .mutex :
217- if path == '/' :
244+ if path in self . directories :
218245 return {'st_mode' : (stat .S_IFDIR | 0o750 ), 'st_nlink' : 2 }
219246
220247 file = self .get_file (path )
221248
222- file_stat = os .stat (file .filepath )
223-
224- st = {key : getattr (file_stat , key ) for key in dir (file_stat ) if not key .startswith ('__' )}
225-
226- st ['st_mode' ] |= stat .S_IFDIR if stat .S_ISDIR (file_stat .st_mode ) else 0
227- st ['st_mode' ] |= stat .S_IFREG if stat .S_ISREG (file_stat .st_mode ) else 0
228- st ['st_mode' ] |= stat .S_IFLNK if stat .S_ISLNK (file_stat .st_mode ) else 0
229-
230- return st
249+ st = os .stat (file .filepath )
250+ return {
251+ 'st_mode' : (stat .S_IFREG | 0o600 ),
252+ 'st_nlink' : 1 ,
253+ 'st_size' : st .st_size ,
254+ }
231255
232256 def readdir (self , path , fh = None ):
233257 """
@@ -238,7 +262,46 @@ def readdir(self, path, fh=None):
238262 :return: A list of directory contents.
239263 """
240264 with self .mutex :
241- return ['.' , '..' ] + [os .path .basename (f ) for f in self .files ]
265+ if path not in self .directories :
266+ raise FuseOSError (errno .ENOENT )
267+ entries = list (self .directories .get (path , set ()))
268+ seen = set (entries )
269+ for f in self .files :
270+ if os .path .dirname (f ) == path :
271+ name = os .path .basename (f )
272+ if name not in seen :
273+ entries .append (name )
274+ seen .add (name )
275+ return ['.' , '..' ] + entries
276+
277+ def mkdir (self , path , mode ):
278+ """
279+ Creates a new directory at the specified path.
280+
281+ :param path: The full path of the directory to create.
282+ :param mode: Permission mode for the new directory (not used in this implementation).
283+ :raises FuseOSError: If the parent directory does not exist.
284+ """
285+ with self .mutex :
286+ parent , name = self ._split_path (path )
287+ if parent not in self .directories :
288+ raise FuseOSError (errno .ENOENT )
289+ self .directories [parent ].add (name )
290+ self .directories [path ] = set ()
291+
292+ def rmdir (self , path ):
293+ """
294+ Removes an existing directory at the specified path.
295+
296+ :param path: The full path of the directory to remove.
297+ :raises FuseOSError: If the directory does not exist or is not empty.
298+ """
299+ with self .mutex :
300+ if path not in self .directories or self .directories [path ]:
301+ raise FuseOSError (errno .ENOTEMPTY )
302+ parent , name = self ._split_path (path )
303+ self .directories [parent ].remove (name )
304+ del self .directories [path ]
242305
243306 def create (self , path , mode ):
244307 """
@@ -248,14 +311,16 @@ def create(self, path, mode):
248311 :param mode: The mode in which the file will be opened.
249312 :return: The file descriptor.
250313 """
251- filename = os .path .basename (path )
252- if not is_valid_uuid4 (filename ):
253- raise FuseOSError (errno .ENOENT )
314+ parent , name = self ._split_path (path )
254315
255316 with self .mutex :
256- file = EphemeralFile (self .storage_directory , filename )
257- file .open ('w' , mode )
258- self .files [path ] = file
317+ if parent not in self .directories :
318+ raise FuseOSError (errno .ENOENT )
319+ self .directories [parent ].add (name )
320+ full_path = os .path .join (parent , name )
321+ file = EphemeralFile (self .storage_directory , str (uuid .uuid4 ()))
322+ file .open ('w' )
323+ self .files [full_path ] = file
259324 return file .fd
260325
261326 def open (self , path , flags ):
@@ -298,6 +363,7 @@ def write(self, path, data, offset, fh=None):
298363 """
299364 with self .mutex :
300365 file = self .get_file (path )
366+ file .seek (offset )
301367 file .write (data )
302368 return len (data )
303369
@@ -308,10 +374,12 @@ def unlink(self, path):
308374 :param path: The file path to remove.
309375 """
310376 with self .mutex :
377+ parent , name = self ._split_path (path )
311378 file = self .files .pop (path , None )
312379 if file :
313380 file .close ()
314381 os .unlink (file .filepath )
382+ self .directories [parent ].discard (name )
315383
316384 def release (self , path , fh = None ):
317385 """
@@ -342,12 +410,11 @@ def truncate(self, path, length, fh=None):
342410 file .seek (length )
343411
344412 if length > file .size :
345- length = length - file .size
346- bytes_written = 0
347- while bytes_written < length :
348- to_write = min (CHUNK_SIZE , length - bytes_written )
413+ pad_len = length - file .size
414+ while pad_len > 0 :
415+ to_write = min (CHUNK_SIZE , pad_len )
349416 file .write (b'\0 ' * to_write )
350- bytes_written + = to_write
417+ pad_len - = to_write
351418
352419 def chmod (self , path , mode ):
353420 """
0 commit comments