Handle Binary Files¶
This guide shows how to serve and fetch binary content like images and archives.
Prerequisites¶
- Mototli installed
- Basic familiarity with the client or server
Serving binary files¶
Binary files are served automatically based on file extension.
MIME type detection¶
Mototli detects file types and maps them to Gopher item types:
| Extension | MIME Type | Item Type |
|---|---|---|
.gif |
image/gif | g (GIF) |
.png, .jpg |
image/* | I (Image) |
.tar, .zip |
application/* | 9 (Binary) |
.mp3, .wav |
audio/* | s (Sound) |
.pdf |
application/pdf | d (Document) |
Add binary files to your gopherhole¶
Auto-generated listings will use the correct item types.
Explicit gophermap entries¶
For manual control, specify the type in your gophermap:
gLogo /logo.gif localhost 7070
IPhoto Gallery /photo.png localhost 7070
9Download Archive /data.tar.gz localhost 7070
sBackground Music /ambient.mp3 localhost 7070
Configure file size limits¶
Prevent serving extremely large files:
Files exceeding this limit return an error.
Fetching binary files with CLI¶
Use the get command with --type 9:
Or use --raw to output binary content:
Fetching binary files with Python¶
Use get_binary() for binary content:
import asyncio
from pathlib import Path
from mototli.client import GopherClient
async def download_file(host: str, selector: str, output: str):
async with GopherClient() as client:
response = await client.get_binary(host, selector)
output_path = Path(output)
output_path.write_bytes(response.content)
print(f"Downloaded {len(response.content)} bytes to {output}")
asyncio.run(download_file("localhost:7070", "/data.tar.gz", "data.tar.gz"))
Download multiple files¶
import asyncio
from pathlib import Path
from mototli.client import GopherClient
async def download_files(host: str, files: list[tuple[str, str]]):
"""Download multiple files from a gopher server.
Args:
host: Server hostname
files: List of (selector, output_filename) tuples
"""
async with GopherClient() as client:
for selector, output in files:
try:
response = await client.get_binary(host, selector)
Path(output).write_bytes(response.content)
print(f"Downloaded: {output} ({len(response.content)} bytes)")
except Exception as e:
print(f"Failed: {selector} - {e}")
files = [
("/images/logo.gif", "logo.gif"),
("/images/banner.png", "banner.png"),
("/archives/data.tar.gz", "data.tar.gz"),
]
asyncio.run(download_files("localhost:7070", files))
Check file size before downloading¶
With Gopher+, you can check size without downloading:
import asyncio
from mototli.client import GopherClient
async def check_and_download(host: str, selector: str, max_size: int = 10_000_000):
"""Download only if file is under max_size bytes."""
async with GopherClient() as client:
# Get attributes first
attrs = await client.get_attributes(host, selector)
if attrs.info and attrs.info.size:
size = attrs.info.size
if size > max_size:
print(f"File too large: {size} bytes (max: {max_size})")
return None
print(f"Downloading {size} bytes...")
response = await client.get_binary(host, selector)
return response.content
asyncio.run(check_and_download("localhost:7070", "/large-file.zip"))
Detect item type from response¶
from mototli.utils import get_item_type, get_mime_type
from pathlib import Path
# From file extension
path = Path("image.png")
item_type = get_item_type(path)
mime_type = get_mime_type(path)
print(f"Item type: {item_type}") # ItemType.IMAGE
print(f"MIME type: {mime_type}") # image/png
Progress indication¶
For large downloads, track progress:
import asyncio
from mototli.client import GopherClient
async def download_with_progress(host: str, selector: str, output: str):
async with GopherClient() as client:
# Get size first (Gopher+ only)
try:
attrs = await client.get_attributes(host, selector)
total_size = attrs.info.size if attrs.info else None
except Exception:
total_size = None
# Download
response = await client.get_binary(host, selector)
# Save
with open(output, "wb") as f:
f.write(response.content)
if total_size:
print(f"Downloaded {len(response.content)}/{total_size} bytes")
else:
print(f"Downloaded {len(response.content)} bytes")
asyncio.run(download_with_progress("localhost:7070", "/file.zip", "file.zip"))
Troubleshooting¶
"File corrupted"¶
Binary files must be fetched with binary mode:
# Correct
response = await client.get_binary(host, selector)
# Wrong - decodes as text, corrupts binary data
response = await client.get_text(host, selector)
"File too large"¶
Increase the server's file size limit:
"Unknown item type"¶
Add the extension to MIME mapping or use explicit item type in gophermap.
See also¶
- Item Types Reference - Complete type list
- Fetch Resources - More fetching patterns
- Protocol API - ItemType enum