22# SPDX-License-Identifier: AGPL-3.0-or-later
33import re
44
5- def get_file_id_from_file_url (file_url ) -> int :
6- # Define the regex pattern to capture only the digits
7- pattern = r"https?://[a-zA-Z-_.:0-9]+/(index\.php/)?f/(\d+)"
8- match = re .search (pattern , file_url )
5+ from nc_py_api import AsyncNextcloudApp
6+ from nc_py_api .files .files_async import AsyncFilesAPI , FsNode
97
8+ TEXT_LIKE_MIMETYPE_PARTS = ('text/' , 'application/json' , 'application/xml' , 'application/xhtml' , '+xml' , '+json' )
9+ MAX_FILE_SIZE = 100_000 # 100kB, approx. 25k tokens
10+
11+ _FILE_ID_SUFFIX_RE = re .compile (r'/(index\.php/)?f/(\d+)/?$' )
12+
13+
14+ def _strip_scheme (url : str ) -> str :
15+ """Remove the http(s):// scheme from a URL for scheme-agnostic comparison."""
16+ return re .sub (r'^https?://' , '' , url )
17+
18+
19+ def is_an_internal_file_link (nc : AsyncNextcloudApp , url : str ) -> bool :
20+ """Check whether a URL is an internal file link for this Nextcloud instance (scheme-agnostic)."""
21+ nc_host = _strip_scheme (nc .app_cfg .endpoint .rstrip ('/' ))
22+ url_without_scheme = _strip_scheme (url )
23+ return url_without_scheme .startswith (nc_host ) and bool (_FILE_ID_SUFFIX_RE .search (url_without_scheme [len (nc_host ):]))
24+
25+
26+ def get_file_id_from_file_url (file_url : str ) -> int :
27+ """Extract the numeric file ID from a Nextcloud-internal file URL."""
28+ match = _FILE_ID_SUFFIX_RE .search (file_url )
1029 if match :
1130 return int (match .group (2 ))
12- else :
13- raise Exception ("Not a valid nextcloud file URL" )
31+ raise Exception ("Not a valid nextcloud file URL" )
32+
33+
34+ def format_fs_node (fsnode : FsNode ) -> dict :
35+ # todo: permissions info
36+ return {
37+ 'path' : fsnode .user_path ,
38+ 'file_id' : fsnode .info .fileid ,
39+ 'etag' : fsnode .etag .replace ('"' , '' ).replace ("'" , '' ),
40+ 'bytes' : fsnode .info .size ,
41+ 'creation_date' : fsnode .info .creation_date .isoformat (),
42+ 'last_modified' : fsnode .info .last_modified .isoformat (),
43+ 'mimetype' : fsnode .info .mimetype ,
44+ 'is_shared' : fsnode .is_shared ,
45+ 'is_favourite' : fsnode .info .favorite ,
46+ 'is_version' : fsnode .info .is_version ,
47+ 'trash_info' : {
48+ 'in_trash' : fsnode .info .in_trash ,
49+ ** ({
50+ 'trashbin_filename' : fsnode .info .trashbin_filename ,
51+ 'original_location' : fsnode .info .trashbin_original_location ,
52+ 'deletion_time' : fsnode .info .trashbin_deletion_time ,
53+ } if fsnode .info .in_trash else {}),
54+ },
55+ 'lock_info' : {
56+ 'is_locked' : fsnode .lock_info .is_locked ,
57+ ** ({
58+ 'owner' : fsnode .lock_info .owner ,
59+ 'owner_display_name' : fsnode .lock_info .owner_display_name ,
60+ 'type' : fsnode .lock_info .type .name ,
61+ 'creation_time' : fsnode .lock_info .lock_creation_time ,
62+ 'ttl' : fsnode .lock_info .lock_ttl ,
63+ 'locked_by_app' : fsnode .lock_info .owner_editor ,
64+ } if fsnode .lock_info .is_locked else {}),
65+ },
66+ }
67+
68+
69+ async def get_file_node (nc : AsyncNextcloudApp , file_id : int ) -> FsNode :
70+ files_handle = AsyncFilesAPI (nc ._session )
71+ node = await files_handle .by_id (file_id )
72+ if not node :
73+ raise RuntimeError (f'No file/folder found with id: { file_id } ' )
74+ return node
75+
76+
77+ async def get_file_contents (nc : AsyncNextcloudApp , fsnode : FsNode ) -> str :
78+ """
79+ RuntimeError: just return the metadata to the model since the node is one of the following:
80+ - a folder
81+ - very large in size
82+ - non-text mimetype
83+ """
84+ if fsnode .is_dir :
85+ raise RuntimeError ('Folder found at the given file id, skipping download' )
86+ if fsnode .info .content_length > MAX_FILE_SIZE :
87+ raise RuntimeError (f'File id { fsnode .info .fileid } is too large to download at { fsnode .info .content_length } bytes' )
88+ if not any (t in fsnode .info .mimetype for t in TEXT_LIKE_MIMETYPE_PARTS ):
89+ raise RuntimeError (f'File id { fsnode .info .fileid } is of content type { fsnode .info .mimetype } so cannot be displayed as text' )
90+ files_handle = AsyncFilesAPI (nc ._session )
91+ return (await files_handle .download (fsnode )).decode (encoding = 'utf-8' , errors = 'ignore' )
92+
93+
94+ async def get_file_content_from_int_link (nc : AsyncNextcloudApp , url : str ) -> str :
95+ file_id = get_file_id_from_file_url (url )
96+ fsnode = await get_file_node (nc , file_id )
97+
98+ try :
99+ return await get_file_contents (nc , fsnode )
100+ except RuntimeError as e :
101+ return f'Failed to download the file/folder: { e } .\n More info about the node:{ format_fs_node (fsnode )} '
0 commit comments