Serwowanie zarchiwizowanych .zip

Serwowanie zarchiwizowanych .zip
Gouda105
  • Rejestracja:ponad 7 lat
  • Ostatnio:9 dni
  • 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:około 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:prawie 7 lat
  • Ostatnio:około 19 godzin
  • 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:9 dni
  • 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:ponad 7 lat
  • Ostatnio:9 dni
  • 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:9 dni
  • 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:ponad 7 lat
  • Ostatnio:9 dni
  • 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:9 dni
  • 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:ponad 7 lat
  • Ostatnio:9 dni
  • 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:9 dni
  • 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:ponad 7 lat
  • Ostatnio:9 dni
  • 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

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)