Carpetas de imágenes por usuario en Node.js

Cuando cada persona escribe su propio contenido, la galería debería mostrar a cada una sus propias imágenes, no las de todos. En el ejemplo de Express, eso es un cambio en una sola función, rootFor(req), que elige la raíz de almacenamiento en cada solicitud. Esta página muestra esa función, en qué orden tiene que ejecutarse respecto a multer y qué no protegen las carpetas por usuario.
Una función elige la raíz
Todo lo que hace la galería (listar, crear carpetas, subir) ocurre dentro de la carpeta que devuelve rootFor(req). De forma predeterminada es la carpeta compartida uploads/. Con PER_USER=1 es 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;
}No cambia nada más. La carpeta drafts de Alice es uploads/users/alice/drafts en el disco, el cuadro de diálogo la llama drafts y la respuesta de la carga es /uploads/users/alice/drafts/forest-5e6f80c8.jpg. La URL funciona porque la raíz por usuario está dentro de UPLOADS_DIR, y el ejemplo construye cada URL a partir de la ruta bajo UPLOADS_DIR. Si devuelves una carpeta fuera de ella, las URL salen mal.
Antes de multer, no después
La raíz se elige en un middleware sobre /rte, antes de 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 });
});El orden importa. Si rootFor() se ejecutara dentro del handler de carga, multer leería primero hasta 4,000,000 bytes de cualquiera, con sesión iniciada o no, y solo entonces se rechazaría la solicitud. Como middleware, lanza GalleryError(401) antes de analizar el cuerpo, y Express pasa el error al middleware de errores. Coloca tu propio middleware de inicio de sesión (sesiones, passport, una comprobación de JWT) delante de él de la misma manera.
De dónde sale el id del usuario
El id debe venir de tu inicio de sesión, nunca de algo que envíe el navegador. demoUser(), en el ejemplo, lee una cookie establecida por /demo-login?user=alice, para que puedas probar con dos usuarios sin un sistema de inicio de sesión. Esa ruta solo existe con PER_USER=1 y solo acepta a-z y 0-9. Cualquiera puede establecer cualquier cookie, así que es un sustituto provisional, no autenticación. Sustituye la llamada por req.user.id, o por lo que te dé tu middleware de sesión. Si ese id no es un token simple (una dirección de correo electrónico, por ejemplo), asócialo primero a un id numérico: se convierte en un nombre de carpeta.
Sin usuario, todas las solicitudes a /rte responden 401 {"error":"Sign in to use the gallery."}. El cuadro de diálogo muestra entonces "The image gallery is unavailable right now (http-401)."
Carpetas compartidas y por usuario en un mismo disco
Las raíces por usuario están en uploads/users/. Si la misma carpeta también se usa en modo compartido, la galería compartida mostraría users y permitiría a cualquiera entrar en las carpetas de todos. Por eso, la raíz compartida trata users como reservado:
// 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 modo compartido, el listado de la raíz omite users, ?folder=users/alice recibe un 404, una carga dentro de ella recibe un 404 y crear una carpeta llamada users recibe un 400. La comparación no distingue mayúsculas y minúsculas, así que Users en Windows también queda cubierto. La imagen del principio muestra ambos en un mismo disco: las carpetas compartidas blog, products y team, y users/alice y users/bob.
Qué no protege
Las imágenes siguen siendo archivos estáticos bajo /uploads/. Abrimos la imagen de Alice sin ninguna cookie y obtuvimos un 200. La parte aleatoria del nombre hace que las URL sean difíciles de adivinar, pero no secretas: están en el HTML de cada documento que muestra la imagen. Si las imágenes deben ser privadas, sustituye el montaje de express.static por una ruta que compruebe el usuario antes de enviar el archivo.
Pruébalo
Inicia el proyecto de ejemplo de esta guía en modo por usuario con PER_USER=1 npm start. En PowerShell, ejecuta primero $env:PER_USER=1 y luego npm start. La opción -b de curl envía la cookie provisional:
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."}En el navegador, abre http://localhost:5101/demo-login?user=alice y haz clic en Insert Gallery. El cuadro de diálogo solo muestra las carpetas y las imágenes de Alice. Abre /demo-login?user=bob y el cuadro de diálogo solo muestra las de Bob.
La misma guía para otras plataformas
PHP · ASP.NET Core · MVC 5 · Web Forms