Cum fac upload la fișiere cu final-form + react-dropzone?

Folosesc bibliotecile astea două:

https://react-dropzone.netlify.com/

Ce vreau eu să fac: lucrez la o aplicație în care se pot adăuga diverse cheltuieli, iar cheltuielile astea trebuie să fie însoțite și de chitanță / factură. Cam așa arată UI:

Default:

Cu fișiere atașate:

Eh, problema de care m-am lovit acum este: cum fac upload la acele fișiere la submit. Soluțiile găsite de mine până acum implică urcarea fișierelor înainte de submit, ceea ce mi se pare ușor stupid. Nu mă interesează preview (altceva decât numele fișierului), mă interesează să pot urca mai multe fișiere, să le pot selecta prin drag&drop ȘI să le pot selecta old school (browse ->select files).

Bun, cum am făcut:

onDrop(files) {
	let uploads = [];
	files.forEach((file) => {
		const reader = new FileReader();
		reader.onload = () => {
			uploads.push({
				name: file.name,
				content: reader.result,
			});

			this.setState({
				files: uploads,
			});
		};

		reader.readAsBinaryString(file);
	});
}

În PHP (folosesc Lumen), fac fișierul așa:

foreach ($request->get('invoices') as $file) {

	$path = 'invoices/'.date('Y/m/d');

	$destinationPath = base_path("public/{$path}/");

	@mkdir($destinationPath, 0755, true);

	$fileName = uniqid() . $file['name'];

	file_put_contents($destinationPath.$fileName, $file['content']);
}

Până aici toate sunt bune: fișierele sunt urcate DAR:

  1. fișierele text sunt OK
  2. Fișierele binare (imaginile) sunt corupte (i.e. nu se deschid cu nimic)
  3. Fișierele urcate sunt mai mari în dimensiune (fișierul original are 800kb, upload are 1mb+)

Ce naiba fac greșit și cum pot face debug la treaba asta? Problema e la citire sau la scriere?

Poate e un pic offtopic, eu unul eu inca lucrez cu Uppy (https://uppy.io/examples/dragdrop/).

Cum spuneam și intr-un post anterior, ei oferă și componenta de client și cea de server. Folosesc protoculul TUS pentru upload de fisiere.

Avantaje :

  • vine la pachet cu o interfață drăguță, dar care poate fi costumizată (css)
  • comportament ușor de customizat (spre exemplu pentru demo cu drag&drop de mai sus poți crea un eveniment separat in urma căruia fișierele să fie efectiv uploadate)
  • intregrate cu multiple servicii (google drive pare interesant)
  • se ocupă de state-ul upload-ului : pause/continue, continuă după page-refresh sau conexiuni picate, poate încarca și fisiere mari.
  • callbacks

Pe partea de server, in cazul meu python, când un fișier este incarcat el primeste un uid. Pentru un upload sunt generate 2 fișiere bazate pe acest uid:

  • dfsdiiu234hiuhsfighdsfg = continutul efectiv
  • dfsdiiu234hiuhsfighdsfg.info = fisier partener .info ce contine meta-informatii
{
    "upload_metadata": {
        "filetype": "video/mp4",
        "filename": "How To Open a Can of Cola.mp4",
        "type": "video/mp4",
        "name": "How To Open a Can of Cocla.mp4"
    },
    "upload_length": 940007,
    "partial": false,
    "parts": null
}

Din ce am testat nu am observat vre-o diferență de calitate sau dimensiune.

Eu m-as uita si la file_put_contents plus log-ul din apache sau ce server folosesti.

Nu am vreo preferință anume în ceea ce privește bibliotecile folosite, deci nu zic nu. Doar că aș vrea ca toată treaba să se facă prin REST, deci Tus… nu prea e ce am nevoie.

Ce nu înțeleg eu este: cum integrez uppy (sau orice alt sistem similar) într-un formular? (e.g. input + select + dropzone).

Adică care e flow-ul? Fac submit în doi pași? Într-un singur pas (caz în care fac eu ceva greșit)?

Probabil voi spune o prostie.
Ai incercat sa iei imaginea din folder(unde ai facut upload), sa o copiezi in alta parte(ex desktop) si apoi sa o deschizi ?

Pune, te rog, codul pe care-l folosesti in Lumen la form submit, este posibil acolo sa fie problema.

La post folosesti multipart/form-data sau application/x-www-form-urlencoded?

The FormData interface provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to “multipart/form-data” .

onDrop(files) {
  this.setState({ files })
}

onFormSubmit () {
  const files = this.state.files; // selected files
  const form = this.state.form; // form fields
  const data = new FormData();

  for (const file of files) {
    data.append('files[]', file, file.name);
  }

  // add here your key / value pair from the form fields
  for (const field in form) {
    data.set(field, form[field]);
  }

  return fetch('https://example.com/api/upload', {
    method: 'POST',
    body: data,      
  });
}
1 Like

Dacă las formular normal, pot folosi $_FILE. Eh, dar pentru că folosesc final-form… nu pot face asta. Pentru că această componentă îmi formează un json, ce-l trimit mai departe cu axios.

Componenta de drag & drop nu generează un field de genul <input type=file> ci îmi dă conținutul fișierului, cu care fac… ce vreau eu.

Codul php este fix cel de mai sus.


Între timp am identificat o altă problemă: encodarea datelor se face greșit. E.g. un fișier text se transferă corect doar dacă nu are diacritice, moment în care lucrurile o iau razna

Poate te ajuta asta:

Păi de acolo am aflat de dropzone! :smiley:

Uite că nu am fost atent la cerințele tale. Nu doar că vrei să faci upload dar ai nevoie și de conținutul formularului, de preferat într-un singur request.

Nu am folosit dar aparent Uppy se integrează și cu formulare de Html (https://uppy.io/docs/form/):

It collects user-specified metadata from form fields, right before Uppy begins uploading/processing files.
It can append upload results back to the form as a hidden field. Currently the appended result is a stringified version of a result returned from uppy.upload() or complete event.

În ceea ce privește protocoalele, TUS nu este singurul. HTML multipart form uploads este și el, pe lângă alte câteva, acceptat.

1 Like

Din codul postat, nu se vede unde trimiti datele catre server.
Cred ca tu trimiti date prin GET. Daca vrei neaparat sa trimiti prin GET, trebuie sa encodezi stringurile binare cu base64 si pe server le decodezi base64->binar. Pentru fisiere e recomandat POST, si trebuie folosit exemplul dat de Eugen mai sus cu FormData.

axios
		.post(expenseEndpoint, data)
		.then((res) => res.data)
		.then((payload) => {
			dispatch({
				type: ADD_EXPENSE,
				payload: payload,
			});
		});

Aplicația este una de test, pe care învăț una-alta. Ce mă interesează pe mine de fapt este și un mod generic în care pot face asta, să pot folosi aceeași idee și în altă parte.

Uppy pare din ce în ce mai tentant, dar cred că nu se împacă prea bine cu final-forms. O să investighez în zilele următoare.

Folosesti FormData si ‘Content-Type’: ‘multipart/form-data’ header?

M-am jucat putin cu Lumen sa vad cum este comparativ cu node.js, am pus mai jos un exemplu de POST /submit si upload handling, sper ca te ajuta.

const data = new FormData();

// files you get from dropzone
for (const file of files) {
  data.append('images[]', file, file.name);
}

// some test data
data.set("name", "John Doe");
data.set("age", "21");

const postOptions = {
  // set the correct content format so the server understands what we send
  headers: {  'Content-Type': 'multipart/form-data'  },
}

// works with the lumen example below
axios.post('/submit', data, postOptions).
  .then((res) => res.data)
  .then((reply) => {
    console.log(reply)
  })

EDIT: Am pus aici exemplul de upload handling cu Lumen. Am testat si fisiere cu diacritice si functioneaza bine.

Ori n-am explicat bine ori nu ai înțeles tu bine :smiley:

Dacă fac formular normal, clasic, html chior, upload funcționează așa cum ar trebui. Problema mea era la integrarea cu dropzone, că nu știam cum aș putea să trimit toată treaba mai departe :slight_smile:

Dar FormData mi-a dat o idee, care mi-a rezolvat problema: parsez tot obiectul trimis de final-form într-un FormData:

const onSubmit = (values, form) => { // handler-ul final-form
  const parsedData = new FormData();

  Object.keys(values).forEach(fieldName => {
    if (typeof values[fieldName] === 'object') {
      for (const value of values[fieldName]) {
        if (value.name) {
          parsedData.append(`${fieldName}[]`, value, value.name);
        } else {
          parsedData.append(`${fieldName}`, value);
        }
      }
    } else {
      parsedData.append(fieldName, values[fieldName], (values[fieldName].name || ''));
    }
  })

  props.handleSubmit(parsedData); // `handleSubmit` face request-ul ajax
};

Mi se pare ușor hacky soluția, dar merge. Ceva îmi spune că toată conversia asta mă va mușca de partea dorsală în viitor, dar… If it looks stupid but works it ain’t stupid.

1 Like

sau sau… da, tu esti de vina

2 Likes

Pentru Base64:

1 Like