tus / tus-node-server

Node.js tus server, standalone or integrable in any framework, with disk, S3, and GGC stores.
https://tus.io/
MIT License
795 stars 197 forks source link

"Store files in custom nested directories" example fails with can't patch error #619

Closed KirioXX closed 3 months ago

KirioXX commented 3 months ago

Hi I try to implement the the nested directory example, but when I try to upload via the uppy tus service I get a can't patch error.

Error: tus: unexpected response while uploading chunk, originated from request (method: PATCH, url: http://127.0.0.1:1080/files/dXNlcnMvdGVzdC9jMjZkYWQyNjIwOGQ4Y2I1Y2JiNzQ0NWU1NGY2MmQzMA, response code: 404, response text: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot PATCH /files/dXNlcnMvdGVzdC9jMjZkYWQyNjIwOGQ4Y2I1Y2JiNzQ0NWU1NGY2MmQzMA</pre>
</body>
</html>
, request id: n/a)
    at Upload2._emitHttpError (@uppy_tus.js?v=64ccd693:1508:23)
    at @uppy_tus.js?v=64ccd693:1728:18

This is my test server:

const { Server } = require('@tus/server');
const { FileStore } = require('@tus/file-store');
const express = require('express');
const cors = require('cors');
const crypto = require('crypto');

const path = '/files';
const server = new Server({
  path,
  datastore: new FileStore({ directory: './files' }),
  namingFunction(req) {
    const id = crypto.randomBytes(16).toString('hex');
    const folder = 'test'; // your custom logic
    return `users/${folder}/${id}`;
  },
  generateUrl(_, { proto, host, path, id }) {
    id = Buffer.from(id, 'utf-8').toString('base64url');
    return `${proto}://${host}${path}/${id}`;
  },
  getFileIdFromRequest(req) {
    const reExtractFileID = /([^/]+)\/?$/;
    const match = reExtractFileID.exec(req.url);

    if (!match || path.includes(match[1])) {
      return;
    }

    return Buffer.from(match[1], 'base64url').toString('utf-8');
  },
});

const host = '127.0.0.1';
const port = 1080;
const app = express();
const uploadApp = express();
app.use(cors());

uploadApp.all('*', server.handle.bind(server));
app.use('/uploads', uploadApp);
app.listen(port, host);

and this is my very basic uppy setup:

import {Dashboard} from '@uppy/react'
import Uppy from '@uppy/core'
import Tus from '@uppy/tus'

import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';

const endpoint = 'http://127.0.0.1:1080/uploads'

function App() {
  const uppy = new Uppy({
    debug: true,
    logger: {
      debug: console.log,
      info: console.info,
      warn: console.warn,
      error: console.error
    },
  }).use(Tus, {
    endpoint,
  });

  return (
    <Dashboard
      uppy={uppy}
      width='100vw'
      height='calc(100vh - 50px)'
      proudlyDisplayPoweredByUppy={false}
    />
  )
}

export default App

Is there any think that I could do wrong that could cause the upload to fail?

Murderlon commented 3 months ago

Hi, might have to do that you are using Uppy in React incorrectly. Uppy can't be declared as a simple variable inside your component. Checkout the docs: https://uppy.io/docs/react/.

KirioXX commented 3 months ago

Hi @Murderlon, thanks for the quick response. I tried to initialise uppy in a useState but I still get the same error. This is the new component:

import {Dashboard} from '@uppy/react'
import Uppy from '@uppy/core'
import Tus from '@uppy/tus'
import React, {useState} from 'react'

import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';

const endpoint = 'http://127.0.0.1:1080/uploads'

function App() {
  const [uppy] = useState(() => new Uppy({
    debug: true,
    logger: {
      debug: console.log,
      info: console.info,
      warn: console.warn,
      error: console.error
    },
  }).use(Tus, {
    endpoint,
  }));

  return (
    <Dashboard
      uppy={uppy}
      width='100vw'
      height='calc(100vh - 50px)'
      proudlyDisplayPoweredByUppy={false}
    />
  )
}

export default App

I'm not sure if I understand the generateUrl and getFileIdFromRequest fully, could it be that the mapping to the file is wrong in one of those functions?

Murderlon commented 3 months ago

I'll see if I can reproduce with a test

Murderlon commented 3 months ago

I know this works for Supabase, as can be seen here: https://github.com/supabase/storage/blob/30351a147e2a5d8d4ca0036c2507597793b78c9c/src/http/routes/tus/lifecycle.ts#L81-L143

Maybe that serves as inspiration in the meantime.

Murderlon commented 3 months ago

I actually already wrote a test for exactly this once:

https://github.com/tus/tus-node-server/blob/a0f9da1450700d97a2e77d791e175ce3adaa1b78/packages/server/test/Server.test.ts#L285-L327

Maybe remove cors from your setup?

KirioXX commented 3 months ago

Removing the cors middleware will result in cors errors on the client. I'm not sure why but I have the feeling that the path mapping is not correct when the request comes in.

I will try to copy the supabase setup and see if that makes a difference. Thanks Murderlon

Murderlon commented 3 months ago

Supabase has a multi-tenant setup so I wouldn't exactly use that. I think your problem is that you use /uploads on the client but /files on the server.

KirioXX commented 3 months ago

That is interesting, does that mean the tus server path has to match the express server path? Because the express is setting the uploadApp to the /uploads endpoint.

Murderlon commented 3 months ago

It should yes, as can be seen from the example.

KirioXX commented 3 months ago

I completely missed that. Thank you very much @Murderlon! 🙌