api.py (6428B)
1 #!/usr/bin/python3 2 3 import asyncio 4 import collections 5 import enum 6 import io 7 import pathlib 8 import random 9 from typing import Any, AsyncIterator, Generic, Iterator, Optional, TypeVar 10 11 # normally I'd prefer using standard modules but streaming POST data is really important 12 import httpx 13 import msgspec 14 15 T = TypeVar("T") 16 17 GofileUpload = collections.namedtuple("GofileUpload", ["file", "result"]) 18 19 20 class GofileStatus(enum.Enum): 21 OK = "ok" 22 23 # header is incorrect 24 # this may happen if uploadFile was called without using multipart/form-data 25 ERROR_HEADERS = "headersError" 26 27 # no file was provided 28 # POST data may be malformed 29 ERROR_NOFILE = "error-noFile" 30 31 # no owner token was provided 32 # happens if a folderId was provided without the correct owner 33 ERROR_OWNER = "error-owner" 34 35 # token is invalid 36 # either a malformed token was initially provided or the guest token expired 37 ERROR_TOKEN = "error-token" 38 39 # token is not associated with a premium user 40 # as the API notes, guest tokens cannot access the API methods marked as premium 41 ERROR_NOT_PREMIUM = "error-notPremium" 42 43 # no server is available to process this request 44 ERROR_NO_SERVER = "noServer" 45 46 47 class GofileServerResult(msgspec.Struct): 48 server: Optional[str] = None 49 50 51 class GofileZonedServerResult(msgspec.Struct): 52 name: Optional[str] = None 53 zone: Optional[str] = None 54 55 def to_base_server(self): 56 return GofileServerResult(self.name) 57 58 59 class GofileServerListResult(msgspec.Struct): 60 servers: list[GofileZonedServerResult] 61 62 63 class GofileUploadResult(msgspec.Struct): 64 download_page: str = msgspec.field(name="downloadPage") 65 code: str = msgspec.field(name="parentFolderCode") 66 parent_folder: str = msgspec.field(name="parentFolder") 67 file_id: str = msgspec.field(name="id") 68 file_name: str = msgspec.field(name="name") 69 md5_hash: str = msgspec.field(name="md5") 70 servers: list[str] 71 72 # a guestToken field is provided if no access token was given and no folderID was specified 73 guest_token: Optional[str] = msgspec.field(default=None, name="guestToken") 74 75 76 class GofileServerResponse(msgspec.Struct, Generic[T]): 77 status: GofileStatus 78 data: T 79 80 81 async def _gofile_api_get(*args, type: type[T], **kwargs) -> T: 82 # performs a GET request and extracts the 'data' property from the response as a given type 83 # if the status is not 'ok', an exception is raised 84 async with httpx.AsyncClient() as client: 85 r = await client.get(*args, **kwargs) 86 87 # suppress the valid-type error since mypy doesn't cannot use runtime-specialized types 88 # but msgspec needs to do so 89 # https://stackoverflow.com/a/59636248 90 result = msgspec.json.decode(r.text, type=GofileServerResponse[type]) # type: ignore[valid-type] 91 92 if result.status != GofileStatus.OK: 93 raise Exception(result) 94 return result.data 95 96 97 async def _gofile_api_post(*args, type: type[T], **kwargs) -> T: 98 # performs a POST request and extracts the 'data' property from the response as a given type 99 # if the status is not 'ok', an exception is raised 100 async with httpx.AsyncClient() as client: 101 r = await client.post(*args, **kwargs) 102 103 # see typing woes at _gofile_api_get 104 result = msgspec.json.decode(r.text, type=GofileServerResponse[type]) # type: ignore[valid-type] 105 106 if result.status != GofileStatus.OK: 107 raise Exception(result) 108 return result.data 109 110 111 async def get_upload_server() -> GofileServerResult: 112 server_list = await _gofile_api_get( 113 "https://api.gofile.io/servers", type=GofileServerListResult 114 ) 115 116 if not server_list.servers: 117 return GofileServerResult() 118 return random.choice(server_list.servers).to_base_server() 119 120 121 async def upload_single( 122 file: io.FileIO, 123 token: Optional[str] = None, 124 folder_id: Optional[str] = None, 125 server: Optional[str] = None, 126 ) -> GofileUpload: 127 """ 128 Uploads a single file. 129 130 :param file: An open file handle. 131 :param token: Token used for uploading. If not specified, the returned 132 ``GofileUpload.result.guest_token`` should be used for subsequent uploads to 133 the same folder. 134 :param folder_id: Folder to upload to. If not specified, the returned 135 ``GofileUpload.result.parent_folder`` should be used for subsequent 136 uploads to the same folder. 137 :param server: A specific subdomain to upload to. 138 """ 139 # we return a GofileUpload instead of a GofileUploadResult so there's consistency between upload_single / upload_multiple 140 141 if not server: 142 upload_server_result = await get_upload_server() 143 server = upload_server_result.server 144 145 # automatically shorten long file names in small terminals (e.g. split panes) 146 while True: 147 try: 148 filepath = pathlib.Path(str(file.name)) 149 post_data: dict[str, Any] = { 150 "file": (filepath.name, file, "application/octet-stream"), 151 } 152 153 if token: 154 post_data["token"] = token 155 if folder_id: 156 post_data["folderId"] = folder_id 157 158 upload_result = await _gofile_api_post( 159 f"https://{server}.gofile.io/contents/uploadfile", 160 files=post_data, 161 type=GofileUploadResult, 162 ) 163 break 164 except httpx.HTTPError as e: 165 print(e) 166 pass 167 168 return GofileUpload(file, upload_result) 169 170 171 async def upload_multiple( 172 files: Iterator[io.FileIO], 173 token: Optional[str] = None, 174 folder_id: Optional[str] = None, 175 server: Optional[str] = None, 176 ) -> AsyncIterator[GofileUpload]: 177 """ 178 Uploads multiple files to the same folder, returning an interator of results. 179 """ 180 first_file, *other_files = files 181 182 if not server: 183 upload_server_result = await get_upload_server() 184 server = upload_server_result.server 185 186 first_upload = await upload_single(first_file, token, folder_id, server) 187 188 if not token: 189 token = first_upload.result.guest_token 190 if not folder_id: 191 folder_id = first_upload.result.parent_folder 192 193 yield first_upload 194 195 uploads = [upload_single(file, token, folder_id, server) for file in other_files] 196 for upload in asyncio.as_completed(uploads): 197 yield await upload