#20.1 – Korzystanie z API: obsługa plików – upload

By 26 lutego 2015Kurs Javy
Wpis-Header lekcje

W poprzedniej lekcji obsługiwaliśmy upload plików, a następnie zapisywaliśmy te pliki na dysku serwera. Dzisiaj nauczymy się robić to prawidłowo ;)

Tak jak wspominaliśmy wcześniej, przechwowywanie plików na serwerze nie jest dobrym pomysłem. Dzisiaj będziemy korzystać z API, żeby nauczyć się 2 rzeczy: jak korzystać z API na podstawie dokumentacji oraz jak prawidłowo obsługiwać pliki w naszej aplikacji.

Wybór API

W tej lekcji będziemy korzystać z API Amazon S3. Będziemy z niego korzystać z kilku powodów: przez pierwszy rok jest bezpłatne (do 5 GB), jest jedną z najpopularniejszych usług w sieci (np. usługa Dropbox, którą prawdopodobnie znasz, korzysta z S3 do przechowywania plików)  i ma bardzo dobrą dokumentację. Żeby nie było, że faworyzujemy jedną firmę, poniżej lista alternatyw – sam proces jest właściwie identyczny, inne będą tylko konkretne klasy, których używamy. Alternatywne rozwiązania:

Zachęcam do eksperymentowania, może się okazać że konkretna usługa jest bardziej dopasowana do Twoich potrzeb a też po prostu warto wiedzieć, jakie są alternatywy jeśli kiedyś miałabyś wybierać technologie do użycia w projekcie :)

Lekcja

Przede wszystkim bardzo ważna bedzie dokumentacja. Prawie każde współczesne API udostępnia krótkie tutoriale, najczęściej opisane jako getting started, takie jak ten: http://aws.amazon.com/s3/getting-started/ .

Wymagania przed lekcją

Przed realizacją tej lekcji konieczne będzie założenie konta w ramach Amazon AWS oraz utworzenie tzw. kubełka. Tutoriale i przewodniki Amazon są naprawdę dobrze napisane i przy znajomości języka angielskiego (lub pomocy translatora) procesy te nie powinny przysporzyć nikomu problemów. W razie gdyby było inaczej – piszcie w komentarzach, będziemy starali się pomóc :)

Biblioteki

API do usług Amazona jest dostępne w formie artefaktu Maven (com.amazonaws:aws-java-sdk). Dodajmy go do naszego projektu jako zależność do modułu services.

Trochę teorii

S3 to API (Application Programmable Interface) pozwalające na przechowywanie (i przesyłanie oraz pobieranie) plików na serwerach firmy Amazon. Usługa ta jest dostępna w wielu miejscach świata (możemy wybrać pomiędzy serwerami rozmieszczonymi na całym świecie, choć w naszym przypadku nie powinno to mieć większego znaczenia, staramy się wybierać lokalizację najbliższą nam – w tym wypadku Frankfurt).

S3 zbudowane jest wokół koncepcji kubełków (buckets) – można o nich myśleć jak o ‚dyskach’ w komputerze – każdy plik musimy umieścić w wybranym kubełku, w ramach kubełka nazwy plików nie mogą się powtarzać.

Nazwy plików zachowują się nieco inaczej niż w przypadku dysków fizycznych – w przypadku S3 nie ma stricte folderów, można za to używać ukośników w nazwach, co w interfejsie webowym jest właśnie reprezentowane jako ‚foldery’.

Poza tym większość konceptów się pokrywa z tym, co znamy z podstawowej obsługi komputera :)

Upload plików

Zajmijmy się na początek uploadem plików na serwer. Dwa elementy mamy już gotowe: nasz kontroler z poprzedniej lekcji oraz dokumentację do API (w tym wypadku skorzystamy z tej konkretnej podstrony)

Kontroler

@RequestMapping(value="/upload", method=RequestMethod.POST)
public String handleFileUpload(@RequestParam("plik") MultipartFile file){
    if (!file.isEmpty()) {
        try {
            UUID uuid = UUID.randomUUID();
            String filename = "/uploads/upload_"+uuid.toString();
            byte[] bytes = file.getBytes();
            File fsFile = new File(filename);
            fsFile.createNewFile();
            BufferedOutputStream stream =
                        new BufferedOutputStream(new FileOutputStream(fsFile));
            stream.write(bytes);
            stream.close();

            logger.info("File {} has been successfully uploaded as {}", new Object[] {file.getOriginalFilename(), filename});
        } catch (Exception e) {
            logger.error("File has not been uploaded", e);
        }
    } else {
        logger.error("Uploaded file is empty");
    }
    return "redirect:/";
}

Przykład z dokumentacji

    public static void main(String[] args) throws IOException {
        AmazonS3 s3client = new AmazonS3Client(new ProfileCredentialsProvider());
        try {
            System.out.println("Uploading a new object to S3 from a file\n");
            File file = new File(uploadFileName);
            s3client.putObject(new PutObjectRequest(
                                     bucketName, keyName, file));

         } catch (AmazonServiceException ase) {
            System.out.println("Caught an AmazonServiceException, which " +
                    "means your request made it " +
                    "to Amazon S3, but was rejected with an error response" +
                    " for some reason.");
            System.out.println("Error Message:    " + ase.getMessage());
            System.out.println("HTTP Status Code: " + ase.getStatusCode());
            System.out.println("AWS Error Code:   " + ase.getErrorCode());
            System.out.println("Error Type:       " + ase.getErrorType());
            System.out.println("Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            System.out.println("Caught an AmazonClientException, which " +
                    "means the client encountered " +
                    "an internal error while trying to " +
                    "communicate with S3, " +
                    "such as not being able to access the network.");
            System.out.println("Error Message: " + ace.getMessage());
        }
    }

Musimy teraz połączyć oba te fragmenty w jeden :)

Jedna zmiana, którą będziemy musieli wykonać to użyta metoda putObject – w powyższym kodzie używamy metody, która przyjmuje plik na dysku. My skorzystamy z wersji, która obsługuje strumienie, opisanej w dokumentacji JavaDoc (http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3Client.html).

Wykorzystamy też inny sposób uwierzytelniania – podając bezpośrednio nasz access key oraz secret key za pomocą klasy AWSCredentials.

Ponieważ w kontrolerze mamy dostęp do uploadowanego pliku jako tablicy bajtów, musimy wykorzystać jeszcze jedną klasę, żeby zamienić tą tablicę na InputStream (oczekiwany przez API S3):

InputStream is = new ByteArrayInputStream(fileBytes);

Z naszych obu przykładów bierzemy więc fragmenty zaznaczone na czerwono i wklejamy ten z przykładu do naszego kodu:

Kontroler

@RequestMapping(value="/upload", method=RequestMethod.POST)
public String handleFileUpload(@RequestParam("plik") MultipartFile file){
    if (!file.isEmpty()) {
        try {
            UUID uuid = UUID.randomUUID();
            String filename = "/uploads/upload_"+uuid.toString();
            byte[] bytes = file.getBytes();
            File fsFile = new File(filename);
            fsFile.createNewFile();
            BufferedOutputStream stream =
                        new BufferedOutputStream(new FileOutputStream(fsFile));
            stream.write(bytes);
            stream.close();

            logger.info("File {} has been successfully uploaded as {}", new Object[] {file.getOriginalFilename(), filename});
        } catch (Exception e) {
            logger.error("File has not been uploaded", e);
        }
    } else {
        logger.error("Uploaded file is empty");
    }
    return "redirect:/";
}

Przykład z dokumentacji

    public static void main(String[] args) throws IOException {
        AmazonS3 s3client = new AmazonS3Client(new ProfileCredentialsProvider());
        try {
            System.out.println("Uploading a new object to S3 from a file\n");
            File file = new File(uploadFileName);
            s3client.putObject(new PutObjectRequest(
                                     bucketName, keyName, file));

         } catch (AmazonServiceException ase) {
            System.out.println("Caught an AmazonServiceException, which " +
                    "means your request made it " +
                    "to Amazon S3, but was rejected with an error response" +
                    " for some reason.");
            System.out.println("Error Message:    " + ase.getMessage());
            System.out.println("HTTP Status Code: " + ase.getStatusCode());
            System.out.println("AWS Error Code:   " + ase.getErrorCode());
            System.out.println("Error Type:       " + ase.getErrorType());
            System.out.println("Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            System.out.println("Caught an AmazonClientException, which " +
                    "means the client encountered " +
                    "an internal error while trying to " +
                    "communicate with S3, " +
                    "such as not being able to access the network.");
            System.out.println("Error Message: " + ace.getMessage());
        }
    }

Innymi słowy – pomijamy wszystkie elementy ‚dookoła’ (informacje wypisywane na konsole, póki co także bloki try-catch; uzupełnimy je później) i przenosimy samą logikę.

Następnie dopasowujemy nazwy zmiennych i uzupełniamy dodatkowymi operacjami, które potrzebujemy żeby dopasować dane w formacie który mamy, do tego akceptowanego przez API (np. przytoczonym wcześniej fragmentem).

W ostatnim kroku uzupełniamy wszystkie bloki try-catch (tutaj najwygodniej pozwolić po prostu Eclipse zrobić to za nas i skorzystać z podpowiadanych rozwiązań). W efekcie otrzymujemy ponizszy kod dla naszego kontrolera:

@RequestMapping(value="/upload", method=RequestMethod.POST)
public String handleFileUpload(@RequestParam("plik") MultipartFile file){
    if (!file.isEmpty()) {
        try {
            UUID uuid = UUID.randomUUID();
            String filename = "/uploads/upload_"+uuid.toString();
			String bucketName = "nazwaKubelka";
			String accessKey = "twojAccessKey";
			String secretKey = "twojSecretKey";
            byte[] bytes = file.getBytes();
			InputStream inputStream = new ByteArrayInputStream(bytes);
            AmazonS3 s3client = new AmazonS3Client(new BasicAWSCredentials(accessKey, secretKey));
			s3client.putObject(new PutObjectRequest(
                                     bucketName, filename, inputStream, new ObjectMetadata()));

            logger.info("File {} has been successfully uploaded as {}", new Object[] {file.getOriginalFilename(), filename});
        } catch (Exception e) {
            logger.error("File has not been uploaded", e);
        }
    } else {
        logger.error("Uploaded file is empty");
    }
    return "redirect:/";
}

Zwróć uwagę, że nie musieliśmy dodawać obsługi wyjątków – są one już łapane przez nasz try-catch, który mieliśmy wcześniej.

Podsumowanie

W dzisiejszej lekcji nauczyliśmy się korzystać z API do umieszczania plików na serwerze. W kolejnej części tej lekcji nauczymy się także pobierać pliki z serwera i wyświetlać je użytkownikom. Powiemy sobie też więcej o tym, gdzie szukać materiałów jeśli mamy problem.

Zadanie

Zmodyfikuj aplikację tak, aby przesyłane przez użytkowników zdjęcia były zapisywane nie na dysku, ale na Amazon S3 (lub innym, wybranym przez Ciebie serwerze plików).

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!

  •  
  •  
  •  
  •  
  •  
  • scrappcio

    Czemu przechowywanie plików na serwerze nie jest dobrym pomysłem??

    • Cześć,
      w poprzedniej lekcji (http://kobietydokodu.pl/19-upload-i-pobieranie-plikow/) dodaliśmy paragraf z podsumowaniem, dlaczego trzymanie plików lokalnie na serwerze może nie być najlepszym pomysłem :) Gdybyś miał wątpliwości lub uwagi – zachęcam do komentowania pod tamtym artykułem.
      Pozdrawiam, Jakub Derda