Dossiers d’images par utilisateur en Node.js

Quand chaque personne rédige son propre contenu, la galerie doit montrer à chacune ses propres images, et non celles de tout le monde. Dans l’exemple Express, cela se résume à la modification d’une seule fonction, rootFor(req), qui choisit la racine de stockage de chaque requête. Cette page présente cette fonction, l’ordre dans lequel elle doit s’exécuter par rapport à multer, et ce que les dossiers par utilisateur ne protègent pas.
Une seule fonction choisit la racine
Tout ce que fait la galerie (liste, création de dossier, envoi) se passe à l’intérieur du dossier renvoyé par rootFor(req). Par défaut, il s’agit du dossier partagé uploads/. Avec PER_USER=1, c’est uploads/users/<id>/ :
// The storage root for this request. Everything the gallery lists, creates or uploads
// stays inside it. Keep it inside UPLOADS_DIR, so the /uploads URLs still work.
// PER_USER=1 gives every user uploads/users/<id>/.
const PER_USER = process.env.PER_USER === "1";
function rootFor(req) {
if (!PER_USER) return UPLOADS_DIR;
const user = demoUser(req); // with real logins: req.user.id from your session middleware
if (!user) throw new GalleryError(401, "Sign in to use the gallery.");
return path.join(UPLOADS_DIR, "users", user);
}
// Stand-in for a login, for trying PER_USER locally: /demo-login?user=alice sets a cookie.
// Anyone can set any cookie, so this is NOT authentication.
function demoUser(req) {
const match = /(?:^|;\s*)demo_user=([a-z0-9]{1,32})(?:;|$)/.exec(req.headers.cookie ?? "");
return match ? match[1] : null;
}Rien d’autre ne change. Le dossier drafts d’Alice est uploads/users/alice/drafts sur le disque, la boîte de dialogue l’appelle drafts, et la réponse à l’envoi est /uploads/users/alice/drafts/forest-5e6f80c8.jpg. L’URL fonctionne parce que la racine par utilisateur se trouve à l’intérieur de UPLOADS_DIR, et que l’exemple construit chaque URL à partir du chemin sous UPLOADS_DIR. Si vous renvoyez un dossier situé en dehors, les URL sont fausses.
Avant multer, pas après
La racine est choisie dans un middleware sur /rte, avant multer :
// Every /rte request gets its storage root first, before multer reads the body.
app.use("/rte", (req, res, next) => {
req.storageRoot = rootFor(req);
next();
});
// GET /rte/gallery?folder=products -> listing
app.get("/rte/gallery", async (req, res) => {
res.json(await listFolder(req.storageRoot, req.query.folder ?? ""));
});
// POST /rte/gallery action=create-folder, folder, name -> listing of the parent folder
app.post("/rte/gallery", parseForm.none(), async (req, res) => {
const { action, folder, name } = req.body ?? {};
if (action !== "create-folder") throw new GalleryError(400, "Unknown action.");
res.json(await createFolder(req.storageRoot, folder ?? "", name));
});
// POST /rte/upload file, folder -> { url }
app.post("/rte/upload", parseForm.single("file"), async (req, res) => {
if (!req.file) throw new GalleryError(400, "No file was uploaded.");
const url = await saveImage(req.storageRoot, req.body.folder ?? "", req.file.originalname, req.file.buffer);
res.json({ url });
});L’ordre compte. Si rootFor() s’exécutait dans le gestionnaire d’envoi, multer lirait d’abord jusqu’à 4,000,000 octets envoyés par n’importe qui, connecté ou non, et ce n’est qu’ensuite que la requête serait refusée. En tant que middleware, la fonction lève GalleryError(401) avant l’analyse du corps, et Express transmet l’erreur au middleware d’erreurs. Placez votre propre middleware de connexion (sessions, passport, vérification de JWT) devant elle de la même façon.
D’où vient l’identifiant de l’utilisateur
L’identifiant doit provenir de votre système de connexion, jamais de quelque chose qu’envoie le navigateur. Le demoUser() de l’exemple lit un cookie défini par /demo-login?user=alice, ce qui vous permet d’essayer deux utilisateurs sans système de connexion. Cette route n’existe qu’avec PER_USER=1 et n’accepte que a-z et 0-9. N’importe qui peut définir n’importe quel cookie : c’est donc un substitut, pas une authentification. Remplacez cet appel par req.user.id, ou par ce que fournit votre middleware de session. Si cet identifiant n’est pas un simple jeton (une adresse e-mail, par exemple), associez-le d’abord à un identifiant numérique : il devient un nom de dossier.
Sans utilisateur, chaque requête /rte répond 401 {"error":"Sign in to use the gallery."}. La boîte de dialogue affiche alors « The image gallery is unavailable right now (http-401). ».
Dossiers partagés et dossiers par utilisateur sur un même disque
Les racines par utilisateur se trouvent dans uploads/users/. Si le même dossier sert aussi en mode partagé, la galerie partagée listerait users et permettrait à n’importe qui de parcourir les dossiers de tout le monde. La racine partagée considère donc users comme réservé :
// Per-user roots live in uploads/users/<id>/. The shared gallery must not show or touch them.
function isUsersFolder(root, folder) {
return path.resolve(root) === UPLOADS_DIR && String(folder ?? "").split("/")[0].toLowerCase() === "users";
}En mode partagé, la liste de la racine omet users, ?folder=users/alice reçoit un 404, un envoi dans ce dossier reçoit un 404, et la création d’un dossier nommé users reçoit un 400. La comparaison ignore la casse : Users sous Windows est donc couvert aussi. L’image en haut de la page montre les deux sur un même disque : les dossiers partagés blog, products et team, et users/alice et users/bob.
Ce que cela ne protège pas
Les images restent des fichiers statiques sous /uploads/. Nous avons ouvert l’image d’Alice sans aucun cookie et obtenu un 200. La partie aléatoire du nom rend les URL difficiles à deviner, mais pas secrètes : elles figurent dans le HTML de chaque document qui affiche l’image. Si les images doivent être privées, remplacez le montage express.static par une route qui vérifie l’utilisateur avant d’envoyer le fichier.
Tester
Démarrez le projet d’exemple de ce guide en mode par utilisateur avec PER_USER=1 npm start. Dans PowerShell, exécutez d’abord $env:PER_USER=1, puis npm start. L’option -b de curl envoie le cookie de substitution :
curl "http://localhost:5101/rte/gallery?folder="
{"error":"Sign in to use the gallery."}
curl -b demo_user=bob -F "file=@photo.jpg" http://localhost:5101/rte/upload
{"url":"/uploads/users/bob/photo-cbf27791.jpg"}
curl -b demo_user=bob "http://localhost:5101/rte/gallery?folder=../alice"
{"error":"Invalid folder."}Dans le navigateur, ouvrez http://localhost:5101/demo-login?user=alice et cliquez sur Insert Gallery. La boîte de dialogue n’affiche que les dossiers et les images d’Alice. Ouvrez /demo-login?user=bob : la boîte de dialogue n’affiche que ceux de Bob.
Le même guide pour d’autres plateformes
PHP · ASP.NET Core · MVC 5 · Web Forms