protocol: download unknown mime types

This commit is contained in:
dece 2021-04-17 22:59:54 +02:00
parent b2fdabea71
commit cf48818c24
6 changed files with 96 additions and 35 deletions

View file

@ -10,15 +10,17 @@ TODO DONE
encodings encodings
bookmarks bookmarks
view/edit sources view/edit sources
downloads
open last download
non shit command-line non shit command-line
home page home page
downloads
media files media files
view history view history
identity management identity management
configuration configuration
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
BACKLOG BACKLOG
download to disk, not in memory
margins / centering margins / centering
pre blocks folding pre blocks folding
buffers (tabs) buffers (tabs)

View file

@ -1,6 +1,9 @@
"""Gemini-related features of the browser.""" """Gemini-related features of the browser."""
from pathlib import Path
from bebop.browser.browser import Browser from bebop.browser.browser import Browser
from bebop.fs import get_downloads_path
from bebop.navigation import set_parameter from bebop.navigation import set_parameter
from bebop.page import Page from bebop.page import Page
from bebop.protocol import Request, Response from bebop.protocol import Request, Response
@ -55,17 +58,10 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True,
if not response: if not response:
browser.set_status_error(f"Server response parsing failed ({url}).") browser.set_status_error(f"Server response parsing failed ({url}).")
return return
response.url = url
if response.code == 20: if response.code == 20:
handle_code = handle_response_content(browser, response) handle_response_content(browser, url, response, history)
if handle_code == 0:
if browser.current_url and history:
browser.history.push(browser.current_url)
browser.current_url = url
browser.cache[url] = browser.page_pad.current_page
browser.set_status(url)
elif handle_code == 1:
browser.set_status(f"Downloaded {url}.")
elif response.generic_code == 30 and response.meta: elif response.generic_code == 30 and response.meta:
browser.open_url(response.meta, base_url=url, redirects=redirects + 1) browser.open_url(response.meta, base_url=url, redirects=redirects + 1)
elif response.generic_code in (40, 50): elif response.generic_code in (40, 50):
@ -78,44 +74,72 @@ def open_gemini_url(browser: Browser, url, redirects=0, history=True,
browser.set_status_error(error) browser.set_status_error(error)
def handle_response_content(browser: Browser, response: Response) -> int: def handle_response_content(browser: Browser, url: str, response: Response,
"""Handle a response's content from a Gemini server. history: bool):
"""Handle a successful response content from a Gemini server.
According to the MIME type received or inferred, render or download the According to the MIME type received or inferred, the response is either
response's content. rendered by the browser, or saved to disk. If an error occurs, the browser
displays it.
Currently only text content is rendered. For Gemini, the encoding specified Only text content is rendered. For Gemini, the encoding specified in the
in the response is used, if available on the Python distribution. For other response is used, if available on the Python distribution. For other text
text formats, only UTF-8 is attempted. formats, only UTF-8 is attempted.
Arguments: Arguments:
- browser: Browser instance that made the initial request.
- url: original URL.
- response: a successful Response. - response: a successful Response.
- history: whether to modify history on a page load.
Returns:
An error code: 0 means a page has been loaded, so any book-keeping such
as history management can be applied; 1 means a content has been
successfully retrieved but has not been displayed (e.g. non-text
content) nor saved as a page; 2 means that the content could not be
handled, either due to bogus MIME type or MIME parameters.
""" """
mime_type = response.get_mime_type() mime_type = response.get_mime_type()
page = None
error = None
filepath = None
if mime_type.main_type == "text": if mime_type.main_type == "text":
if mime_type.sub_type == "gemini": if mime_type.sub_type == "gemini":
encoding = mime_type.charset encoding = mime_type.charset
try: try:
text = response.content.decode(encoding, errors="replace") text = response.content.decode(encoding, errors="replace")
except LookupError: except LookupError:
browser.set_status_error("Unknown encoding {encoding}.") error = f"Unknown encoding {encoding}."
return 2 else:
browser.load_page(Page.from_gemtext(text)) page = Page.from_gemtext(text)
return 0
else: else:
text = response.content.decode("utf-8", errors="replace") text = response.content.decode("utf-8", errors="replace")
browser.load_page(Page.from_text(text)) page = Page.from_text(text)
return 0
else: else:
pass # TODO filepath = get_download_path(url)
return 1
if page:
browser.load_page(page)
if browser.current_url and history:
browser.history.push(browser.current_url)
browser.current_url = url
browser.cache[url] = page
browser.set_status(url)
elif filepath:
try:
with open(filepath, "wb") as download_file:
download_file.write(response.content)
except OSError as exc:
browser.set_status_error(f"Failed to save {url} ({exc})")
else:
browser.set_status(f"Downloaded {url} ({mime_type.short}).")
elif error:
browser.set_status_error(error)
def get_download_path(url: str) -> Path:
"""Try to find the best download file path possible from this URL."""
download_dir = get_downloads_path()
url_parts = url.rsplit("/", maxsplit=1)
if url_parts:
filename = url_parts[-1]
else:
filename = url.split("://")[1] if "://" in url else url
filename = filename.replace("/", "_")
return download_dir / filename
def handle_input_request(browser: Browser, from_url: str, message: str =None): def handle_input_request(browser: Browser, from_url: str, message: str =None):

View file

@ -4,6 +4,7 @@ A lot of logic comes from `appdirs`:
https://github.com/ActiveState/appdirs/blob/master/appdirs.py https://github.com/ActiveState/appdirs/blob/master/appdirs.py
""" """
from functools import lru_cache
from os import getenv from os import getenv
from os.path import expanduser from os.path import expanduser
from pathlib import Path from pathlib import Path
@ -12,7 +13,28 @@ from pathlib import Path
APP_NAME = "bebop" APP_NAME = "bebop"
@lru_cache(None)
def get_user_data_path() -> Path: def get_user_data_path() -> Path:
"""Return the user data directory path.""" """Return the user data directory path."""
path = Path(getenv("XDG_DATA_HOME", expanduser("~/.local/share"))) path = Path(getenv("XDG_DATA_HOME", expanduser("~/.local/share")))
return path / APP_NAME return path / APP_NAME
@lru_cache(None)
def get_downloads_path() -> Path:
"""Return the user downloads directory path."""
xdg_config_path = Path(getenv("XDG_CONFIG_HOME", expanduser("~/.config")))
download_path = ""
try:
with open(xdg_config_path / "user-dirs.dirs", "rt") as user_dirs_file:
for line in user_dirs_file:
if line.startswith("XDG_DOWNLOAD_DIR="):
download_path = line.rstrip().split("=", maxsplit=1)[1]
download_path = download_path.strip('"')
download_path = download_path.replace("$HOME", expanduser("~"))
break
except OSError:
pass
if download_path:
return Path(download_path)
return Path.home()

View file

@ -13,6 +13,10 @@ class MimeType:
sub_type: str sub_type: str
parameters: dict parameters: dict
@property
def short(self):
return f"{self.main_type or '*'}/{self.sub_type or '*'}"
@property @property
def charset(self): def charset(self):
return self.parameters.get("charset", DEFAULT_CHARSET) return self.parameters.get("charset", DEFAULT_CHARSET)

View file

@ -182,7 +182,16 @@ class StatusCode(IntEnum):
@dataclass @dataclass
class Response: class Response:
"""A Gemini response.""" """A Gemini response.
Response objects can be created only by parsing a Gemini response using the
static `parse` method, so you're guaranteed to have a valid object.
Attributes:
- code: the status code returned by the server.
- meta: optional meta content.
- content: bytes as returned by the server, only in successful requests.
"""
code: StatusCode code: StatusCode
meta: str = "" meta: str = ""

View file

@ -18,7 +18,7 @@ def render_lines(metalines, window, max_width):
- window: window that will be resized as filled with rendered lines. - window: window that will be resized as filled with rendered lines.
- max_width: line length limit for the pad. - max_width: line length limit for the pad.
Return: Returns:
The tuple of integers (error, height, width), error being a non-zero value The tuple of integers (error, height, width), error being a non-zero value
if an error occured during rendering, and height and width being the new if an error occured during rendering, and height and width being the new
dimensions of the resized window. dimensions of the resized window.
@ -53,7 +53,7 @@ def render_line(metaline, window, max_width):
window.addstr(url_text, attributes) window.addstr(url_text, attributes)
def get_base_line_attributes(line_type): def get_base_line_attributes(line_type) -> int:
"""Return the base attributes for this line type. """Return the base attributes for this line type.
Other attributes may be freely used later for this line type but this is Other attributes may be freely used later for this line type but this is