Pastas de imagens por usuário em Node.js

A pasta uploads do exemplo em Express, com as pastas compartilhadas blog, products e team e uma pasta users contendo alice e bob, cada um com as próprias imagens

Quando cada pessoa escreve o próprio conteúdo, a galeria deve mostrar a cada uma as suas próprias imagens, e não as de todo mundo. No exemplo em Express, isso é uma mudança em uma única função, rootFor(req), que escolhe a raiz de armazenamento de cada requisição. Esta página mostra essa função, a ordem em que ela precisa ser executada em relação ao multer e o que as pastas por usuário não protegem.

Uma função escolhe a raiz

Tudo o que a galeria faz (listar, criar uma pasta, enviar) acontece dentro da pasta que rootFor(req) retorna. Por padrão, é a pasta compartilhada uploads/. Com PER_USER=1, é uploads/users/<id>/:

JavaScript · server.mjs
// 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;
}

Nada mais muda. A pasta drafts da Alice é uploads/users/alice/drafts no disco, a caixa de diálogo a chama de drafts e a resposta do upload é /uploads/users/alice/drafts/forest-5e6f80c8.jpg. A URL funciona porque a raiz por usuário fica dentro de UPLOADS_DIR, e o exemplo monta todas as URLs a partir do caminho abaixo de UPLOADS_DIR. Se você retornar uma pasta fora dele, as URLs saem erradas.

Antes do multer, não depois

A raiz é escolhida em um middleware em /rte, antes do multer:

JavaScript · server.mjs
// 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 });
});

A ordem importa. Se rootFor() fosse executada dentro do handler de upload, o multer primeiro leria até 4,000,000 bytes de qualquer pessoa, autenticada ou não, e só então a requisição seria recusada. Como middleware, ela lança GalleryError(401) antes de o corpo ser processado, e o Express entrega o erro ao middleware de erro. Coloque o seu próprio middleware de login (sessões, passport, uma verificação de JWT) antes dele da mesma forma.

De onde vem o id do usuário

O id deve vir do seu login, nunca de algo que o navegador envia. O demoUser() do exemplo lê um cookie definido por /demo-login?user=alice, para que você possa testar dois usuários sem um sistema de login. Essa rota só existe com PER_USER=1 e aceita apenas a-z e 0-9. Qualquer pessoa pode definir qualquer cookie, então isso é um substituto provisório, não autenticação. Troque a chamada por req.user.id ou pelo que o seu middleware de sessão fornecer. Se esse id não for um token simples (um endereço de e-mail, por exemplo), mapeie-o primeiro para um id numérico: ele vira um nome de pasta.

Sem usuário, toda requisição a /rte responde 401 {"error":"Sign in to use the gallery."}. A caixa de diálogo então mostra "The image gallery is unavailable right now (http-401)." (a galeria de imagens está indisponível no momento).

Pastas compartilhadas e por usuário no mesmo disco

As raízes por usuário ficam em uploads/users/. Se a mesma pasta também for usada no modo compartilhado, a galeria compartilhada listaria users e deixaria qualquer pessoa navegar pelas pastas de todo mundo. Por isso, a raiz compartilhada trata users como reservado:

JavaScript · gallery.mjs
// 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";
}

No modo compartilhado, a listagem da raiz omite users, ?folder=users/alice recebe 404, um upload para dentro dela recebe 404 e criar uma pasta chamada users recebe 400. A comparação ignora maiúsculas e minúsculas, então Users no Windows também está coberto. A imagem no topo mostra os dois no mesmo disco: as pastas compartilhadas blog, products e team, e users/alice e users/bob.

O que isso não protege

As imagens continuam sendo arquivos estáticos em /uploads/. Abrimos a imagem da Alice sem nenhum cookie e recebemos um 200. A parte aleatória do nome torna as URLs difíceis de adivinhar, mas não secretas: elas estão no HTML de todo documento que mostra a imagem. Se as imagens precisam ser privadas, substitua a montagem do express.static por uma rota que verifique o usuário antes de enviar o arquivo.

Teste

Inicie o projeto de exemplo deste guia no modo por usuário com PER_USER=1 npm start. No PowerShell, execute primeiro $env:PER_USER=1 e depois npm start. O -b do curl envia o cookie provisório:

Shell
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."}

No navegador, abra http://localhost:5101/demo-login?user=alice e clique em Insert Gallery. A caixa de diálogo mostra apenas as pastas e imagens da Alice. Abra /demo-login?user=bob e a caixa de diálogo mostra apenas as do Bob.

O mesmo guia para outras plataformas

PHP · ASP.NET Core · MVC 5 · Web Forms

Leia a seguir