Pusta nazwa przesłanego pliku z transakcjami

Problem

Jeden z integratorów zgłosił następujący problem:

Pliki przesłane przez API mają pustą nazwę (a na stronie www nazwa prezentowana jest jako null).

Przyczyna problemu

Ani aktualna Ustawa, ani rozporządzenia wykonawcze nie narzucają nazewnictwa plików przesyłanych do GIIF. Dlatego API nie wymusza podawania nazwy pliku. Plik przesłany przez przeglądarkę ma domyślnie przekazaną nazwę pliku, ale przy wywołaniach API, oprogramowanie musi wprost wskazać nazwę pliku.

Rozwiązanie

W dokumentacji API znajdziemy wzmiankę wskzaującą na wykorzystanie nagłówka Content-Disposition: „Nazwę pliku można przekazać ustawiając nagłówek Content-Disposition na attachment z nazwą pliku w polu filename lub filename*”.

Przykład

Bez polskich znaków

W tym miejscu odwołam się do kwietniowego wpisu Flow-część-3 W tamtym wpisie wysyłałem plik o nazwie gotowka.xml.enc do instytucji o nipie 0123456789, jednakże oryginalne wywołanie:

1
2
curl --data-binary @gotowka.xml.enc \
https://test.giif.mofnet.gov.pl/api/rest2018/instytucje/0123456789/pliki/

nie przesyłało nazwy pliku. Dodaję więc nagłówek:

1
2
3
curl --data-binary @gotowka.xml.enc \
--header 'Content-Disposition: attachment; filename="gotowka.xml.enc"' \
https://test.giif.mofnet.gov.pl/api/rest2018/instytucje/0123456789/pliki/

W rezultacie otrzymuję następującego XMLa:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<plk:plik xmlns:plk="http://www.giif.mofnet.gov.pl/xsd/rest/pliki20171017"
id="7659"
nazwa="gotowka.xml.enc"
hashZaszyfrowany="47F0D3338A4DDE8657B49F5B9CFAC8C088C27BDB4855C7BC1E99EBC94C125C06"
status="Z"
dataOtrzymania="2019-12-13T07:44:31"/>

Jak widać pojawiła się nazwa pliku.

Z polskimi znakami

Powyższy przykład działa wyłącznie wtedy, gdy nazwa pliku ogranicza się do znaków ASCII. Zastosowanie polskich znaków w nazwach plików jest możliwe, ale wymaga zastosowania atrybutu filename* w nagłówku Content-Disposition.

Atrybut filename* zawiera nazwę kodowania, a następnie nazwę pliku w tym kodowaniu, z bajtami spoza zakresu drukowalnych znaków ASCII zapisanymi jako hex encoded binary.

Na przykład nazwa pliku: test-aącćeęlłnńoósśzźzż-AĄCĆEĘLŁNŃOÓSŚZŹZŻ.xml zostanie zamieniona na: UTF-8''test-a%c4%85c%c4%87e%c4%99l%c5%82n%c5%84o%c3%b3s%c5%9bz%c5%baz%c5%bc-A%c4%84C%c4%86E%c4%98L%c5%81N%c5%83O%c3%93S%c5%9aZ%c5%b9Z%c5%bb.xml

Przykładowe wywołanie curl.

1
2
3
curl --data-binary @gotowka.xml.enc \
--header "Content-Disposition: attachment; filename=\"test-aacceel_nnoosszzzz-AACCEEL_NNOOSSZZZZ.xml\"; filename*=UTF-8''test-a%c4%85c%c4%87e%c4%99l%c5%82n%c5%84o%c3%b3s%c5%9bz%c5%baz%c5%bc-A%c4%84C%c4%86E%c4%98L%c5%81N%c5%83O%c3%93S%c5%9aZ%c5%b9Z%c5%bb.xml" \
https://test.giif.mofnet.gov.pl/api/rest2018/instytucje/0123456789/pliki/

Warto zwrócić uwagę na użycie cudzysłowów zamiast apostrofów do otoczenia nagłówka Content-Disposition. Atrybut filename* używa apostrofów, a w bashu cytowanie apostrofami wyłącza escape-sequences. Zamiast tego użyłem cydzysłowów i zacytowałem cudzysłowy w atrybucie filename. Po przetworzeniu przez basha, faktyczna wartość przekazana po parametrze --header będzie miała postać: Content-Disposition: attachment; filename="test-aacceel_nnoosszzzz-AACCEEL_NNOOSSZZZZ.xml"; filename*=UTF-8''test-a%c4%85c%c4%87e%c4%99l%c5%82n%c5%84o%c3%b3s%c5%9bz%c5%baz%c5%bc-A%c4%84C%c4%86E%c4%98L%c5%81N%c5%83O%c3%93S%c5%9aZ%c5%b9Z%c5%bb.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<plk:plik xmlns:plk="http://www.giif.mofnet.gov.pl/xsd/rest/pliki20171017"
id="7660"
nazwa="test-aącćeęlłnńoósśzźzż-AĄCĆEĘLŁNŃOÓSŚZŹZŻ.xml"
hashZaszyfrowany="47F0D3338A4DDE8657B49F5B9CFAC8C088C27BDB4855C7BC1E99EBC94C125C06"
status="Z"
dataOtrzymania="2019-12-13T08:18:31"/>

Dla piszących w Javie dorzucam klasę narzędziową, której używam do generownia nazw plików:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package pl.gov.mofnet.giif.rest.api;

import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.Arrays;

public class ApiUtils {

/**
* Tworzy zawartość nagłówka Content-Disposition dla załącznika o wskazanej nazwie.
*
* @param fileName nazwa pliku, którego nazwa ma być przekazana
* @return treść nagłówka Content-Disposition dla wskazanej nazwy pliku
*/
public static String buildContentDispositionFromFileName(String fileName) {
return "attachment; filename=\"" + ApiUtils.toAscii(fileName) + "\";"
+ " filename*=" + ApiUtils.encodeRFC5987(fileName);
}

/**
* Koduje nazwę pliku w RFC5987 dla pola filename*
*
* @param s Nazwa pliku do zakodowania.
* @return Zakodowana nazwa pliku.
*/
private static String encodeRFC5987(final String s) {
final byte[] rawBytes = s.getBytes(StandardCharsets.UTF_8);
final int len = rawBytes.length;
final StringBuilder sb = new StringBuilder(len << 1);
sb.append("UTF-8''");
final char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
final byte[] attributeChars = {
'!', '#', '$', '&', '+', '-', '.',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'^', '_', '`',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'|', '~'
};
for (final byte b : rawBytes) {
if (Arrays.binarySearch(attributeChars, b) >= 0) {
sb.append((char) b);
} else {
sb.append('%');
sb.append(digits[15 & (b >>> 4)]);
sb.append(digits[b & 15]);
}
}
return sb.toString();
}

/**
* Koduje nazwę pliku dla pola filename.
*
* Usuwa znaki diakrytyczne, zamienia znaki spoza ASCII na podkreślenia.
*
* @param s Nazwa pliku do zakodowania.
* @return Zakodowana nazwa pliku.
*/
private static String toAscii(final String s) {
return Normalizer.normalize(s, Normalizer.Form.NFD)
.replaceAll("\\p{InCombiningDiacriticalMarks}+", "")
.replaceAll("[^a-zA-Z0-9._\\-+,@$!~'=()\\[\\]{}]", "_");
}
}