W tym wpisie skupimy się na omówieniu obsługi plików, wykorzystując do tego platformę Firebase. Pozwala ona na przechowywanie danych multimedialnych za pomocą Google Cloud Storage. Utworzymy prostą aplikację, która umożliwi dodawanie, podgląd oraz usuwanie plików graficznych. Użytkownik będzie informowany o postępie podczas wysyłania pliku za pomocą progress baru.
Jedną z większych zalet Cloud Storage jest przesyłanie i pobieranie niezależne od jakości sieci. Dzięki temu w przypadku przerwania operacji, wznawiana jest ona w tym samym miejscu, oszczędzając czas i transfer klienta. Warto również wspomnieć, że usługa ta jest wysoko skalowalna, dzięki czemu można ją wykorzystywać do prototypowania, jak również w finalnej aplikacji dostępnej dla użytkowników. Wykorzystuje tę samą infrastrukturę co Spotify czy Google Photos.
Za bezpieczeństwo plików odpowiadają reguły, które działają w podobny sposób, jak ma to miejsce w przypadku reguł dla Cloud Firestore. Domyślnie, tylko zalogowani użytkownicy mogą odczytywać i zapisywać dane na serwerze Firebase. Dodatkowo, tworząc reguły dla plików, mamy możliwość odczytu ich metadanych w celu określenia dostępności.
Pliki przechowywane są w strukturze hierarchicznej, podobnie jak pliki na dysku twardym. Poniżej znajduje się przykładowa struktura przechowywanych plików.
- users -- user_id --- file1.png --- file2.png --- file3.png --- ...
Ścieżka do pliku file1.png
users/user_id/file1.png
Referencje
Referencje można określić jako wskaźnik do pliku w chmurze. Za ich pomocą wykonywane są wszystkie operacje na plikach. Referencje mogą odwoływać się do pliku lub do wyższego węzła. Ścieżki do plików przypominają ścieżki do danych w Realtime Database. Poniżej znajdują się przykłady tworzenia referencji.
const fileRef1 = firebase.storage().ref(); // referencja do korzenia const fileRef2 = firebase.storage().ref('files'); // referencja do katalogu files const fileRef3 = firebase.storage().ref('files/img1.png'); // referencja do pliku img1.png
Odwołanie do pliku pozwala na:
- przesyłanie,
- pobieranie,
- pobieranie metadanych,
- modyfikowanie metadanych,
- usuwanie plików.
W aplikacji możemy tworzyć wiele referencji. Warto pamiętać o tym, że odwołania są lekkie dla aplikacji. Dodatkowo, Firebase udostępnia w swojej bibliotece narzędzia do poruszania się po utworzonej strukturze plików za pomocą:
- child() – metoda ta odwołuje się do lokalizacji będącej niżej w drzewie;
- parent – właściwość ta odwołuje się do lokalizacji będącej wyżej w drzewie;
- root – właściwość ta odwołuje się do głównego katalogu – korzenia.
Powyższe metody i właściwości możemy łączyć wiele razy, w celu nawigowania po strukturze plików. Po wywołaniu właściwości parent w katalogu głównym zwrócony zostanie null.
Mamy również do dyspozycji właściwości pozwalające odczytać informacje o pliku na podstawie referencji:
- fullPath – zwraca ścieżkę do pliku;
- name – zwraca nazwę pliku na która wskazuje referencja;
- bucket – zwraca nazwę przestrzeni w której znajduje się plik.
Ograniczenia referencyjne
Tworząc strukturę plików w Cloud Storage należy pamiętać o pewnych ograniczeniach, jakie posiada ta usługa:
- długość ścieżki referencyjnej musi być nie większa niż 1024 bajty w przypadku kodowania UTF-8;
- Nie wolno używać znaku powrotu karetki i nowego wiersza;
- Unikać używania #, [, ], *, ?, ponieważ mogą generować problemy w działaniu innych usług Firebase.
Prezentacja aplikacji
Poniżej znajdują się zrzuty ekranu przedstawiające sposób działania aplikacji.
Widok Dashboardu podczas przesyłania pliku
Widok podglądu
Widok usuwania pliku
Implementacja
Aplikacja ta będzie oparta na mechanizmach wykorzystywanych w poprzednich wpisach cyklu o Firebase, dlatego postanowiłem ominąć omówienie implementacji interfejsu.
Implementacje zaczniemy od utworzenia klasy File, która będzie odpowiadała za przechowywanie nazwy i ścieżki do pliku.
class File { constructor(name, fullPath) { this.name = name; this.fullPath = fullPath; } }
Następnie dodamy moduł, który pozwoli nam wykonywać operacje na plikach. Poniższy kod dodajemy do pliku index.html.
<script src="https://www.gstatic.com/firebasejs/6.3.3/firebase-storage.js"></script>
Wysyłanie
Zaczniemy od utworzenia metody odpowiedzialnej za dodawanie plików na serwer Firebase.
static uploadFile(file) { const fileRef = firebase.storage().ref(`users/${firebase.auth().currentUser.uid}/${file.name}`); const uploadTask = fileRef.put(file); uploadTask.on('state_changed', snapshot => { const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; UI.updateProgressbar(progress); }, error => { console.log(error); }, success => { const file = new File(uploadTask.snapshot.metadata.name, uploadTask.snapshot.metadata.fullPath); UI.addFileToHTML(file); UI.resetProgressbar(); }); }
W parametrze metody uploadFile() przekazujemy plik, który został dodany przez użytkownika w inpucie. Następnie na podstawie identyfikatora użytkownika oraz nazwy wybranego pliku tworzymy referencję, dzięki temu każdy użytkownik będzie posiadał osobny katalog na pliki. Ważne by pamiętać o tym, że referencja musi zawierać nazwę pliku wraz z rozszerzeniem.
Kolejnym krokiem będzie utworzenie zmiennej uploadTask, która będzie przechowywał obiekt zwrócony przez metodę put(), wykorzystywaną do przesyłania pliku. Dzięki obiektowi zachowanemu w zmiennej uploadTask, mamy dostęp do obserwatora oraz metod służących do zarządzania przesyłaniem.
Mamy możliwość zarządzania przesyłaniem plików za pomocą trzech metod:
- pause() – odpowiada za wstrzymanie przesyłania;
- resume() – odpowiada za wznowienie przesyłania;
- cancel() – odpowiada za anulowanie przesyłania.
Obserwator state_changed może znajdować się w trzech stanach:
- running – przesyłanie zostało uruchomione w momencie rozpoczęcia lub wznowienia przesyłania;
- progress – pliki są przesyłane;
- pause – przesyłanie zostało wstrzymane.
Wywołanie metody on() na obiekcie uploadTask wymaga podania czterech parametrów:
- obserwatora;
- metody wywoływanej przy każdej zmianie stanu;
- metody wywoływanej w przypadku zakończenia przesyłania niepowodzeniem;
- metody wywoływanej w przypadku pomyślnego zakończenia przesyłania pliku.
Metoda wywołana przy zmianie stanu, zwraca obiekt TaskSnapshot, który przechowuje informacje o aktualnym stanie przesyłanego pliku. Poniżej znajduje się lista wszystkich właściwości, jakie posiada obiekt TaskSnapshot.
- bytesTransfered – łączna liczba bajtów przesłanych;
- totalBytes – liczba bajtów do przesłania;
- state – aktualny stan przesyłania;
- metadata – metadane, które przed zakończeniem przesyłania zostaną przesłane na serwer Firebase;
- task – obiekt UploadTask, który pozwala na zatrzymania, wznowienie i anulowanie przesyłania;
- ref – referencja z której pochodzi zadanie przesyłania.
W kodzie powyżej metoda wywoływana przy zmianie stanu przesyłania odpowiada za obliczenie ile procent pliku zostało przesłane oraz uaktualnienie progress baru. W przypadku niepowodzenia operacji dostajemy komunikat z błędem w konsoli. Gdy wysyłanie zakończy się sukcesem tworzymy obiekt klasy File, dodajemy nowy element do listy plików i resetujemy progress bar.
Firebase pozwala nam na przesyłanie różnego rodzaju plików. Poniżej znajdują się niektóre z nich:
- File;
- Blob;
- UintSArray;
- raw,
- base64.
Do przesyłania zakodowanego ciągu znaków służy metoda putString(). Metoda ta, podobnie jak put(), zwraca obiekt UploadTask.
Zarówno w przypadku metody put(), jak również putString(), mamy możliwość przesłania naszych metadanych, w tym także własnych pól istotnych dla pliku. Jednak tworzenie własnych pól w metadanych nie jest zalecane, zdecydowanie lepiej do tego wykorzystać inne usługi dostępne w Firebase, np. RTDB. Pole contentType jest określane automatycznie na podstawie rozszerzenia pliku. W przypadku braku rozszerzenia ustawiany jest domyślny typ – application/octet-stream.
Pobieranie
Mając już gotową obsługę wysyłania plików do Cloud Storage, możemy teraz zająć się pobieraniem wszystkich plików danego użytkownika.
static getFiles() { const fileRef = firebase.storage().ref(`users/${firebase.auth().currentUser.uid}`); fileRef.listAll().then(function (res) { res.items.forEach(function (itemRef) { const file = new File(itemRef.name, itemRef.fullPath); UI.addFileToHTML(file); }); }).catch(function (error) { console.log(error); }); }
Tworzymy referencje do katalogu, w którym znajdują się wszystkie pliki użytkownika, a następnie na referencji wywołujemy metodę listAll(), która zwraca dwa obiekty: items i prefixes. Pierwszy obiekt przechowuje pliki, drugi informacje o podkatalogach jakie występują w bieżącej lokalizacji. W powyższym kodzie wykonujemy pętle forEach po wszystkich plikach. Dla każdego pliku tworzymy obiekt klasy File, a następnie wywołujemy metodę odpowiedzialną za dodawanie pliku do listy. W przypadku błędu wykonana zostaje instrukcja catch, która wyświetli w konsoli komunikat o błędzie zwrócony przez Firebase.
Aby móc korzystać z metody listAll() oraz list(), musimy ustawić 2 wersję reguł bezpieczeństwa dla Cloud Storage. Warto również pamiętać, że wynik otrzymany za pomocą tych metod jest buforowany w pamięci. Dlatego zaleca się stosowanie metody listAll(), kiedy mamy małą liczbę danych, w przypadku większych należy zastosować metodę list(), która pozwala na stronicowanie pobieranych plików.
Pobieranie pliku
Posiadając już pliki na serwerze oraz listę plików na stronie, możemy zająć się podglądem plików. Na początek musimy utworzyć referencję do istniejącego pliku. Możemy tego dokonać za pomocą trzech sposobów. Pierwszy z nich to taki, jaki robiliśmy dotychczas. Drugi – za pomocą gs://, zaś trzeci – za pomocą https://. Poniżej znajduje się przykład tworzenia referencji.
const pathReference = storage.ref('images/stars.jpg'); const gsReference = storage.refFromURL('gs://bucket/images/stars.jpg') const httpsReference = storage.refFromURL('https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg');
Mając utworzoną referencję, możemy teraz wywołać na niej metodę getDownloadURL(), która zwraca bezpośredni link do pliku. Mając ten link możemy zainicjować pobieranie pliku za pomocą XMLHttpRequest lub wykorzystać go, np. do wyświetlenia wczytanego obrazu na stronie. Poniżej znajduje się kod odpowiedzialny za ustawienie atrybutu src w podglądzie pliku.
static previewFile(path) { firebase.storage().ref(path).getDownloadURL().then(url => UI.setImg(url)); }
Usuwanie pliku
Usuwanie pliku polega na utworzeniu referencji do pliku, a następnie wywołaniu na niej metody delete(). Poniżej znajduje się kod odpowiedzialny za usuwanie pliku.
static deleteFile() { if (!UI.getID()) { return; } firebase.storage().ref(UI.getID()).delete() .then(res => { UI.removeFile(UI.getID()); }) .catch(function (error) { console.log('Blad', error); }); }
Na początek sprawdzana jest ścieżka do pliku zwracana przez metodę getID(), jeżeli jest niezdefiniowana, to metoda kończy się. W przeciwnym wypadku tworzona jest referencja za pomocą wartość zwróconej przez getID(), a następnie wywołana metoda delete(). W przypadku powodzenia operacji plik jest usuwany z listy plików. W przeciwnym wypadku w konsoli zostaje wyświetlony komunikat z błędem.
Podsumowanie
Jak mogliśmy zobaczyć w tym wpisie, obsługa plików za pomocą Firebase jest bardzo prosta w implementacji. Platforma pozwala nam na zarządzanie przesyłanymi plikami, odnajdywanie plików pod daną ścieżką, pobieranie bezpośredniego adresu, jak również usuwanie. Do dyspozycji mamy również możliwość stronicowania listy plików, bez pobierania wszystkich na raz, a także pobieranie i modyfikowanie metadanych plików. Dużym atutem Cloud Storage jest skalowalność oraz wznawianie operacji w przypadku utracenia połączenia, a nie rozpoczynanie jej na nowo.
Link do repozytorium z kodem aplikacji – L4_storage