Coverage for model_workflow/utils/httpsf.py: 0%
61 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 10:54 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 10:54 +0000
1#!/usr/bin/env python
3import os
4import sys
5import requests
6import stat # For file mode constants
7from fuse import FUSE, Operations # FUSE bindings with fusepy
10# Set the functions which are set by default when the file handler is passed to the fuse class
11# These functions are better not overwritten in the file handler
12# These functions were found experimentally but there may be more
13default_fuse_functions = ['init', 'destroy', 'getattr', 'access']
15# Set the functions which the fuse standards may expect to find in any compliant class and are not yet implemented
16# These functions are set as whistleblowers
17# These functions were found in the internet:
18# https://www.cs.hmc.edu/~geoff/classes/hmc.cs135.201109/homework/fuse/fuse_doc.html
19# https://www.cs.hmc.edu/~geoff/classes/hmc.cs135.201109/homework/fuse/fuse_doc.html#gotchas
20# https://libfuse.github.io/doxygen/structfuse__operations.html
21not_implememnted_fuse_functions = ['fgetattr', 'readlink', 'opendir', 'readdir', 'mknod', 'mkdir', 'unlink',
22 'rmdir', 'symlink', 'rename', 'link', 'chmod', 'chown', 'truncate', 'ftruncate', 'utimens', 'open', 'read',
23 'write', 'statfs', 'release', 'releasedir', 'fsync', 'fsyncdir', 'flush', 'lock', 'bmap', 'setxattr',
24 'getxattr', 'listxattr', 'removexattr', 'ioctl', 'poll', 'create', 'openat', 'write_buf', 'read_buf',
25 'flock', 'fallocate', 'copy_file_range', 'lseek']
27# FUSE implementation for a single file
28class FileHandler(Operations):
29 def __init__ (self, url : str):
30 print("Initializing API Virtual File System...")
31 self.url = url
32 self._response = None
33 # Set all missing fuse functions as whistleblowers
34 # Most commands will return a clueless 'function not implemented' error when trying to access any of these functions
35 # This prevents the error and intead tells the user which function is being called and not yet implemented
36 # This double function is to avoid overwritting the same function every time
37 def create_whistleblower (name : str):
38 def whistleblower (*args):
39 raise SystemExit('FUSE function not implemented: ' + name)
40 return whistleblower
41 for function_name in not_implememnted_fuse_functions:
42 if hasattr(self, function_name):
43 continue
44 setattr(self, function_name, create_whistleblower(function_name))
46 def __call__ (self, function_name : str, *args, **kwargs):
47 function = getattr(self, function_name)
48 return function(*args)
50 def _get_file_size(self):
51 response = requests.head(self.url, allow_redirects=True)
52 response.raise_for_status() # Raise an exception for HTTP errors
53 # Check if Content-Length header exists
54 size = int(response.headers.get('Content-Length', 0))
55 return size
57 def _fetch_file_content(self):
58 """Fetch content from remote API."""
59 self._response = requests.get(self.url)
60 self._response.raise_for_status()
62 # Attribute keys
63 # Got from https://github.com/skorokithakis/python-fuse-sample/blob/master/passthrough.py
64 stat_keys = 'st_atime', 'st_ctime', 'st_mtime', 'st_gid', 'st_uid', 'st_mode', 'st_nlink', 'st_size'
66 # This must be implemented, although there is a default function
67 # FUSE relies in this function a lot and not implementing it properly leads to silent errors
68 def getattr (self, path, fh=None):
69 # Get the stat of current directory to get time and id values
70 dot_st = os.lstat('.')
71 # Example regular file:
72 # (st_mode=33188, st_ino=13765084, st_dev=66306, st_nlink=1, st_uid=16556, st_gid=500, st_size=9410,
73 # st_atime=1696945741, st_mtime=1696945600, st_ctime=1696945600)
74 # Example fifo
75 # (st_mode=4516, st_ino=13765935, st_dev=66306, st_nlink=1, st_uid=16556, st_gid=500, st_size=0,
76 # st_atime=1697017261, st_mtime=1697017261, st_ctime=1697017261)
77 st = dict((key, getattr(dot_st, key)) for key in self.stat_keys)
78 # Set a size bigger than the expected response, no matter how much bigger
79 # DANI: Intenté muchas formas de que la size fuese nula o infinita para que no haya limite, pero no pude
80 # DANI: Prové a cambiar el st_mode para convertir el punto de montaje en ostras cosas, pero solo puede ser file
81 #stat['st_size'] = sys.maxsize
82 st['st_size'] = self._get_file_size()
83 # Set a custom mode for FUSE to treat it as a file with infinite size
84 # To see the actual meaningful value of st_mode you have to do 'oct(st_mode)'
85 # e.g. regular file: 33188 -> 0o100644, fifo: 4516 -> 0o10644
86 # Last three numbers stand for the permissions
87 # The starting numbers stand for the file type
88 # UNIX file type docs
89 # https://man7.org/linux/man-pages/man7/inode.7.html (section 'The file type and mode')
90 # Set the mode for a FIFO
91 st['st_mode'] = 0o644 | stat.S_IFIFO
92 return st
94 # File methods
95 # ============
97 # Open the URL request
98 def open (self, path : str, flags) -> int:
99 # Lazy load content if not already fetched
100 if self._response is None:
101 self._fetch_file_content()
102 # Return a file descriptor (dummy in this case)
103 return 0
105 # Read length is set automatically by FUSE
106 # This length is calculated from the getattr 'st_size' value
107 # However the length is not the exact value of st_size, but the first multiple of 4096 which exceeds its value
108 # Also length has a maximum value: 131072, experimentally observed
109 # When the maximum value is not enought to cover the length the read function is called several times
110 # Each time with different offset value
111 def read (self, path : str, length : int, offset : int, fh : int) -> str:
112 if self._response is None:
113 self._fetch_file_content()
114 return self._response.content[offset:offset + length]
116 # Close the URL request
117 def destroy (self, path : str):
118 if self._response:
119 self._response.close()
121 # The following functions are not implemented
122 # However they must be defined or FUSE complains when trying to read the file
123 flush = None
124 release = None
125 lock = None
126 # Note the FUSE will complain for several functions to be not implemented when trying to write in the file
127 # User should never try to write this file so these cases are not handled
129# Mount a fake file which returns an url content when read
130def mount (url : str, mountpoint : str):
131 # Create a fake file to be opened in case it does not exists yet
132 handler = FileHandler(url)
133 # Create the mount point in case it does not exist yet
134 if not os.path.exists(mountpoint):
135 open(mountpoint, 'w').close()
136 # Mount the handler in the mount point
137 FUSE(handler, mountpoint, nothreads=True, foreground=True)
139if __name__ == '__main__':
140 # Example usage
141 url = 'https://cineca.mddbr.eu/api/rest/v1/projects/A0001/files/structure.pdb'
142 mountpoint = '/home/rchaves/Downloads/structure.pdb'
143 mount(url, mountpoint)