Serwowanie zarchiwizowanych .zip

Serwowanie zarchiwizowanych .zip
Gouda105
  • Rejestracja:prawie 8 lat
  • Ostatnio:około miesiąc
  • Postów:487
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:

Kopiuj
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ź:

Kopiuj
{
    "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:

Kopiuj
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

edytowany 1x, ostatnio: Riddle
G8
  • Rejestracja:ponad 7 lat
  • Ostatnio:ponad rok
  • Postów:85
0

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

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

Wiktor Zychla
  • Rejestracja:około 7 lat
  • Ostatnio:dzień
  • Postów:77
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

edytowany 1x, ostatnio: Riddle
dzek69
Moderator
  • Rejestracja:ponad 18 lat
  • Ostatnio:około miesiąc
  • Lokalizacja:Rzeszów
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:

Kopiuj
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


edytowany 1x, ostatnio: dzek69
Gouda105
Przepraszam, ale zapomniałem dodać, że korzystam z Reacta. Taki zapis wydaje mi się być sprzeczny z jego założeniami. Czy mam rację?
Gouda105
  • Rejestracja:prawie 8 lat
  • Ostatnio:około miesiąc
  • Postów:487
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.

dzek69
Moderator
  • Rejestracja:ponad 18 lat
  • Ostatnio:około miesiąc
  • Lokalizacja:Rzeszów
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


edytowany 1x, ostatnio: dzek69
Gouda105
  • Rejestracja:prawie 8 lat
  • Ostatnio:około miesiąc
  • Postów:487
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:

Kopiuj
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:

Kopiuj
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ć.

dzek69
Przy SSR head w zasadzie też jest pierwotnie renderowany przez React, tylko potem już nie. Ale pomijając nawet tę mało znaczącą dygresję - React bezpośrednio do <body> też nie jest renderowany, zazwyczaj jest jakiś <div id=root> i to w nim sobie żyje sam React. Przykład - pewnie jest lepszy, ale musiałbym wybadać. Taki Swiper.js jest i dobrym i złym przykładem - w kodzie nie widać za wiele odniesień do DOM bezpośrednio - bo wszystko jest już ukryte w "core": https://github.com/nolimits4web/swiper/blob/master/src/react/swiper.js
dzek69
https://github.com/nolimits4web/swiper/tree/master/src/core tutaj core, wielka libka pełna szaleństw w DOM. Jednak ciężej to pokazać na potrzeby wpisu na forum
dzek69
Moderator
  • Rejestracja:ponad 18 lat
  • Ostatnio:około miesiąc
  • Lokalizacja:Rzeszów
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.


edytowany 1x, ostatnio: dzek69
Gouda105
  • Rejestracja:prawie 8 lat
  • Ostatnio:około miesiąc
  • Postów:487
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?

dzek69
Moderator
  • Rejestracja:ponad 18 lat
  • Ostatnio:około miesiąc
  • Lokalizacja:Rzeszów
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


Gouda105
  • Rejestracja:prawie 8 lat
  • Ostatnio:około miesiąc
  • Postów:487
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

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.