gofile

Module and tool to upload files to gofile.io
git clone https://code.alwayswait.ing/gofile
Log | Files | Refs

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