Skip to content

feat(panel): the file editor opens anything that is text, not a list of extensions - #131

Merged
aaldersondev merged 1 commit into
mainfrom
feat/edit-any-text-file
Sep 5, 2026
Merged

feat(panel): the file editor opens anything that is text, not a list of extensions#131
aaldersondev merged 1 commit into
mainfrom
feat/edit-any-text-file

Conversation

@aaldersondev

Copy link
Copy Markdown
Contributor

Ouvrir un dossier de .lua Garry's Mod dans le gestionnaire de fichiers ne proposait que des téléchargements. La page d'édition existait pourtant déjà — c'est la porte qui était fermée :

// apps/panel/web/src/pages/ServerFiles.tsx, avant
const EDITABLE = /\.(ya?ml|properties|json|txt|log|toml|conf|cfg|ini|sh|md|xml|csv)$/i;

Quatorze extensions, et une liste d'autorisation se comporte comme une liste d'autorisation. .lua, .sk d'un Skript, .mcfunction d'un datapack, un bot Python, un Dockerfile, un .env : du texte parfaitement éditable, refusé parce que personne n'avait pensé à l'inscrire, et aucun moyen pour la personne qui regarde le fichier d'insister.

La règle est inversée, et coupée en deux entre les endroits qui peuvent répondre honnêtement.

Le nom ne sert plus qu'à écarter

BINARY_FILE_EXTENSIONS nomme ce qui n'est certainement pas du texte — archives, images, sons, code compilé, bases de données, données de monde d'un jeu. Tout le reste va à l'éditeur, y compris un fichier sans aucune extension. La constante vit dans @hopper/shared, à côté de MAX_EDITABLE_FILE_BYTES : le front et le daemon doivent s'accorder dessus.

Les octets tranchent

files/contents lit les 8000 premiers octets et répond 415 file_not_text si un octet NUL s'y trouve.

C'est l'heuristique de git, et elle est choisie pour être étroite. Refuser l'UTF-8 invalide à la place refuserait le server.properties en Latin-1 dont une décennie de serveurs Minecraft est remplie — un fichier que les gens éditent légitimement et qu'aucun autre outil n'a jamais contesté. Un octet NUL, lui, n'apparaît dans aucun texte que quelqu'un a voulu écrire.

La tête est lue séparément du flux qui sert le fichier : la réponse est nécessaire avant l'envoi, et un flux déjà parti ne peut plus changer son code de statut. Réenfiler le morceau déjà consommé devant le flux aurait mis un emballage entre Fastify et le descripteur — c'est lui qui le referme quand un client raccroche en cours de route.

Et quand ça arrive quand même

L'éditeur transforme le refus en une phrase et un lien de téléchargement, dans les cinq langues, plutôt qu'en code de statut. Deux chaînes restées en français sur cette page sont traduites au passage.

Vérifié

Six tests sur packages/shared (le prédicat, dont le Latin-1 et le fichier vide) et six nouveaux sur la route du daemon — apps/daemon/src/routes/files.spec.ts, contre un vrai système de fichiers temporaire : un .lua servi en 200, un Dockerfile aussi, un fichier à NUL refusé en 415, du Latin-1 servi, un fichier vide servi, un dossier toujours refusé en 400.

format:check, lint 7/7, typecheck 7/7, build 5/5, test 7/7.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HiQhbiTpvjFnK7CJcJuR9m

…of extensions

The editor was reached through an allow list of fourteen extensions, and
it behaved as an allow list does. A folder of Garry's Mod `.lua` offered
nothing but downloads; so did a Skript `.sk`, a datapack `.mcfunction`,
a Python bot, a `Dockerfile`, a `.env`. All of it plain text, all of it
refused because nobody had thought to name it, and no way for the person
looking at the file to say "open it anyway".

The rule is now the other way round, and split between the two places
that can answer it honestly.

**The name only rules things out.** `BINARY_FILE_EXTENSIONS` names what
is certainly not text — archives, images, sounds, compiled code,
databases, a game's world data — and everything else goes to the editor,
including a file with no extension at all. It lives in `@hopper/shared`
next to `MAX_EDITABLE_FILE_BYTES`, since the front end and the daemon
have to agree on it.

**The bytes decide.** `files/contents` reads the first 8000 bytes and
answers 415 `file_not_text` when a NUL byte is in them — git's
heuristic, and chosen for being narrow: refusing invalid UTF-8 instead
would refuse the Latin-1 `server.properties` that a decade of Minecraft
servers are full of, which is a file people legitimately edit. The head
is read separately from the stream that serves the file, because the
answer is needed before the reply is sent and a stream already being
sent can no longer change its status code.

The editor turns that refusal into a sentence and a download link rather
than a status code, in the five languages. Two strings that had stayed
in French on that page are translated with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HiQhbiTpvjFnK7CJcJuR9m
@aaldersondev
aaldersondev merged commit c83234f into main Sep 5, 2026
3 checks passed
@aaldersondev
aaldersondev deleted the feat/edit-any-text-file branch September 5, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant