Serwowanie zarchiwizowanych .zip

0

Witam,
tworzę API, które ma tworzyć wiele Bufferów plików, zapisywać je w .zip i wysyłać na frontend w celu ich pobrania.
Jak na razie udało mi się napisać coś takiego:

const generateResults = async (req, res) => {
	const template = req.file.buffer;
	const data = JSON.parse(req.body.data);
	const suffix = req.body.suffix;
	const fileName = req.body.fileName;

	const zip = new JSZip();

	for (let i = 0; i < data.length; i++) {
		const doc = await handler.process(template, data[i]);
		zip.file(`${fileName} ${data[i][suffix]}`, doc);
	}

	zip.generateAsync({type: 'nodebuffer'}).then(function (content) {
		res.setHeader('Content-disposition', 'attachment; filename=plik.zip');
		res.setHeader('Content-Type', 'application/zip');
		res.write(content, function (err) {
			res.end();
		});
	});
};

Jednak zamiast od razu pobierać pliku, jest wysyłana odpowiedź:

{
    "data": "PK\u0003\u0004\n\u0000\u0000\u0000\u0000\u0000\u0019s�VH�]\u0010?+\u0000\u000...,
    "status": 200,
    "statusText": "OK",
    "headers": {
        "content-type": "application/zip"
    },
    "config": {
        "transitional": {
            "silentJSONParsing": true,
            "forcedJSONParsing": true,
            "clarifyTimeoutError": false
        },
        "adapter": [
            "xhr",
            "http"
        ],
        "transformRequest": [
            null
        ],
        "transformResponse": [
            null
        ],
        "timeout": 0,
        "xsrfCookieName": "XSRF-TOKEN",
        "xsrfHeaderName": "X-XSRF-TOKEN",
        "maxContentLength": -1,
        "maxBodyLength": -1,
        "env": {},
        "headers": {
            "Accept": "application/json, text/plain, */*"
        },
        "method": "post",
        "url": "http://localhost:3001/generateResults",
        "data": {}
    },
    "request": {}
}

Dodam jeszcze moje wywołanie Axios, może będzie potrzebne:

try {
	const response = await axios.post('http://localhost:3001/generateResults', formData, {
		headers: {
			'Content-Type': 'multipart/form-data',
		},
	});
	console.log(response);
	return response;
} catch (error) {
	console.log(error);
}

Z tego co wiem, dodanie nagłówka 'attachment; filename=plik.zip' powinno automatycznie pobrać plik, a to się nie dzieje

0

Ja używałem tego do zapisywania pdf.
https://www.npmjs.com/package/file-saver

Może z zipami też działa...

0

To jest prawidłowa odpowiedź, zgodna z formatem odpowiedzi Axios (https://axios-http.com/docs/res_schema). W szczególności, response.data zawiera odpowiedź serwera (widać preambułę ZIP, PK...) a pozostałe właściwości to m.in. status odpowiedzi itd. Samo generowanie ZIP na serwerze jest prawidłowe.

Żeby taki POST z Axiosa w przeglądarce zamienił się na monit o zapisanie pliku, trzeba odpowiedź serwera w przeglądarce przepakować na coś co przeglądarka potraktuje jako interakcję z użytkownikiem, mniej więcej coś takiego https://gist.github.com/javilobo8/097c30a233786be52070986d8cdb1743

1

axiosem nie wywołasz tego okna. Musi nastąpić przeglądarkowa nawigacja pod dany adres. Ponieważ u Ciebie jest to POST to sprawa się trochę komplikuje, ale dopóki nie masz w form data plików to wszystko będzie w porządku:

const postNavigate = (url: string, formData: FormData, encType = "multipart/form-data") => {
    const form = document.createElement("form");
    form.style.display = "none";
    form.method = "post";
    form.action = url;
    form.enctype = encType;
    const entries = formData.entries();
    for (const [key, value] of entries) {
        if (typeof value !== "string") {
            throw new Error("FormData must contain only string values");
        }

        const input = document.createElement("input");
        input.name = key;
        input.value = value;
        form.appendChild(input);
    }

    document.body.appendChild(form);
    form.submit();
    document.body.removeChild(form);
};

Jeżeli masz pliki - to pozostań przy klasycznym formularzu.

Wytnij sobie z powyższego przykładu typy TypeScriptowe. Wrzucam w takiej formie, bo w takiej powędruje to do mojej libki frontowych utilsów

0

Faktycznie, ten kod robi to, co chciałem, ale problem jest taki, że po zamianie w kodzie na ".zip" i otwarciu pokazuje się błąd, "Nieoczekiwany koniec archiwum" w WinRar.

1

Odpisuję w poście dla przejrzystości @Gouda105.

Użycie ww kodu nie jest sprzeczne z ideą Reacta. Stworzenie prostych elementów, żeby je potem wyrzucić na zawsze zrobione w "czystym js" (z DOM) zamiast w React będzie:

  • łatwiejsze do zamknięcia w formie takiej prostej funkcji, którą wywołujesz gdzie potrzebujesz
  • szybsze w działaniu (choć to znikomy efekt co prawda)
  • prostsze w zapisie (renderowanie forma na chwilę, jego przesłanie imperatywnie (tego nie unikniesz), usunięcie forma z DOM będzie fatalnie wyglądać jako Reactowy komponent)

To nie jest tak, że wszystko, co ma "document", to jest od razu zakazane w React, po prostu bardzo niewskazanym jest modyfikowanie tym elementów stworzonych przez Reacta (ponieważ React nie jest świadomy tych zmian, więc albo coś nie zadziała, albo się nadpisze albo scrashuje), chyba, że bardzo wiesz co robisz. A tu dodatkowo tworzymy coś sami dla siebie, doklejamy do <body>, które jest poza Reactową kontrolą.

Sporo bibliotek do Reacta pod spodem może być nawet prymitywnym wrapperem na kod pełen odniesień do DOM, tylko jeszcze o tym nie wiesz :)

Dla uwiarygodnienia pierwszy lepszy przykład jaki mi przyszedł do głowy: https://www.npmjs.com/package/react-helmet - kiedyś super popularna, teraz to nie wiem, bo dawno nie aktualizowana biblioteka, nadal 1,7 mln pobrań tygodniowo, a co w kodzie? https://github.com/nfl/react-helmet/blob/master/src/HelmetUtils.js bezpośrednie działania na DOM

0

@dzek69: Bardzo dziękuję za odpowiedź. Ostatecznie skorzystałem z rozwiązania @Wiktor Zychla oraz kilku wpisów w na stackoverflow.
Najpierw pokażę, jak rozwiązałem problem, żeby osoby z tym samym problemem znalazły rozwiązanie.

Na początku zmieniłem bibliotekę z jsZip na adm-zip w moim kodzie Node.js.
Następnie zmodyfikowałem kod serwera w taki sposób:

const generateResults = async (req, res) => {
	const template = req.file.buffer;
	const data = JSON.parse(req.body.data);
	const suffix = req.body.suffix;
	const fileName = req.body.fileName;

	var zipper = new zip();

	for (let i = 0; i < 3; i++) {
		const doc = await handler.process(template, data[i]);
		zipper.addFile(`${fileName} - ${data[i][suffix]}.docx`, doc);
	}

	const buffer = zipper.toBuffer();
	res.send(buffer);
};

ważnym było dodanie rozszerzenia .docx do nazwy pliku. Teraz przesyłam cały .zip w formie buffera na frontend.
Na frontendzie (React) wysyłam żądanie, pobieram jego wynik, generuję link do pobierania, automatycznie go naciskam, a następnie usuwam:

try {
  const response = await axios.post('http://localhost:3001/generateResults', formData, {
		headers: {
			'Content-Type': 'multipart/form-data',
		},
		responseType: 'blob',
	});
			
	const url = window.URL.createObjectURL(new Blob([response.data]));
	const link = document.createElement('a');
	link.href = url;
	link.setAttribute('download', 'file.zip');
	document.body.appendChild(link);
	link.click();
    document.body.removeChild(link);
} catch (error) {
	console.log(error);
 }

W moim przypadku działa jak powinno.

Jeśli chodzi o podejście, że wszystko, co ma "document" jest zakazane to faktycznie w trakcie nauki Reacta gdzieś takie sformułowanie podchwyciłem i się w głowie zakotwiczyło. Co do Twojego przykładu to (chyba) nie jest on najbardziej trafny, bo wydaje mi się, że nie można zastąpić <head> komponentem react, bo jest on zawarty w pliku index.html, do którego po za .App za bardzo komponentu nie wstawimy. Jednak wiem, że to był pierwszy lepszy przykład, który przyszedł Ci do głowy i rozumiem co chciałeś przez to przekazać. Muszę tylko się trochę powymądrzać.

1

W sumie to moment, bo Cię oszukałem.

Jakoś mi wyleciało z głowy, że przeglądarki wspierają Bloby 🤦
I z takiego bloba da się wywołać okno zapisu pliku, sam tego gdzieś używałem, tylko nie pamiętam gdzie, pomocna tu będzie biblioteka: https://github.com/eligrey/FileSaver.js - co prawda tutaj jesteś bardziej ograniczony rozmiarem niż powyższą metodą, ale jest to alternatywa.

A jak z axiosa wydobyć bloba to nie wiem, nie lubię axiosa, ale myślę, że znajdziesz.

Edit: ok, widzę, że masz sposób na bloba, opcja z download też jest spoko, nie przemyślałem, że można w ten sposób obskoczyć POSTa.

Nadal jednak najlepsze wydajnościowo rozwiązanie to to, co podałem, nie musisz ładować całej treści pliku do pamięci przeglądarki.

0

No to jednak skorzystam z Twojego rozwiązania. Docelowy plik zip będzie zawierał w sobie nawet około 100 plików .docx i nie chcę zaśmiecać nimi przeglądarki.
Jednak jest pewien problem. W moim formData znajduje się plik .docx bezpośrednio przesłany z input file. Twój kod sprawdza, czy wartości są ciągami znaków, a ja chcę przesłać plik. I stąd pytanie - powinienem konwertować plik na ciąg binarny, przesyłać na serwer i tam z powrotem przetwarzać, czy lepiej pozbyć się walidacji i obsłużyć przesyłanie pliku poprzez wykrycie czy aktualnie iterowany obiekt FormData jest plikiem?

1

Moje rozwiązanie nie pozwala na przesłanie pliku, więc w tej sytuacji pozostań przy tym, co już masz.

A swoją drogą - jeżeli używasz linka z atrubutem download to przetestuj to sobie na Firefox - IIRC FF nie pozwala na ustalenie nazwy pliku przez Ciebie i sam proponuje coś na podstawie URL, ale u Ciebie URL jest generowany z bloba, więc tam będą jakieś śmieci. Żeby się nie okazało, że potem na FF ktoś tego używa, proponuje mu się nazwę pliku typu fa6565a6-da7648-bc2104 (bez rozszerzenia) i potem nie wiadomo jak to otworzyć.

W tym przypadku filesaver.js może być lepszą opcją, ale też już niewiele pamiętam

1

Pozostałem przy tym co było, jednak dodałem jeszcze URL.revokeObjectURL(objectURL), żeby oczyścić pamięć przeglądarki.
https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL

Jeśli chodzi o nazwę zipa to mi na tym nie zależy, bo program jest dla mojej mamy, żeby zaoszczędziła kilka godzin w pracy, a tam i tak używa Chrome.
Tak czy siak bardzo dziękuję za pomoc

1 użytkowników online, w tym zalogowanych: 0, gości: 1