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

By 26 February 2015 February 27th, 2015 Kurs Javy

W poprzed­niej lekcji obsługi­wal­iśmy upload plików, a następ­nie zapisy­wal­iśmy te pli­ki na dysku ser­w­era. Dzisi­aj nauczymy się robić to prawidłowo ;)

Tak jak wspom­i­nal­iśmy wcześniej, przech­wowywanie plików na ser­w­erze nie jest dobrym pomysłem. Dzisi­aj będziemy korzys­tać z API, żeby nauczyć się 2 rzeczy: jak korzys­tać z API na pod­staw­ie doku­men­tacji oraz jak praw­idłowo obsługi­wać pli­ki w naszej aplikacji.

Wybór API

W tej lekcji będziemy korzys­tać z API Ama­zon S3. Będziemy z niego korzys­tać z kilku powodów: przez pier­wszy rok jest bezpłatne (do 5 GB), jest jed­ną z najpop­u­larniejszych usług w sieci (np. usłu­ga Drop­box, którą praw­dopodob­nie znasz, korzys­ta z S3 do prze­chowywa­nia plików)  i ma bard­zo dobrą doku­men­tację. Żeby nie było, że faworyzu­je­my jed­ną fir­mę, poniżej lista alter­natyw — sam pro­ces jest właś­ci­wie iden­ty­czny, inne będą tylko konkretne klasy, których uży­wamy. Alter­naty­wne rozwiązania:

Zachę­cam do ekspery­men­towa­nia, może się okazać że konkret­na usłu­ga jest bardziej dopa­sowana do Twoich potrzeb a też po pros­tu warto wiedzieć, jakie są alter­naty­wy jeśli kiedyś miałabyś wybier­ać tech­nolo­gie do uży­cia w projekcie :)

Lekcja

Przede wszys­tkim bard­zo waż­na bedzie doku­men­tac­ja. Praw­ie każde współczesne API udostęp­nia krótkie tuto­ri­ale, najczęś­ciej opisane jako get­ting start­ed, takie jak ten: http://aws.amazon.com/s3/getting-started/ .

Wymagania przed lekcją

Przed real­iza­cją tej lekcji konieczne będzie założe­nie kon­ta w ramach Ama­zon AWS oraz utworze­nie tzw. kubeł­ka. Tuto­ri­ale i prze­wod­ni­ki Ama­zon są naprawdę dobrze napisane i przy zna­jo­moś­ci języ­ka ang­iel­skiego (lub pomo­cy trans­la­to­ra) pro­cesy te nie powin­ny przys­porzyć niko­mu prob­lemów. W razie gdy­by było inaczej — pisz­cie w komen­tarzach, będziemy starali się pomóc :)

Biblioteki

API do usług Ama­zona jest dostęp­ne w formie arte­fak­tu Maven (com.amazonaws:aws-java-sdk). Doda­jmy go do naszego pro­jek­tu jako zależność do mod­ułu services.

Trochę teorii

S3 to API (Appli­ca­tion Pro­gram­ma­ble Inter­face) pozwala­jące na prze­chowywanie (i przesyłanie oraz pobieranie) plików na ser­w­er­ach firmy Ama­zon. Usłu­ga ta jest dostęp­na w wielu miejs­cach świa­ta (może­my wybrać pomiędzy ser­w­era­mi rozmieszc­zony­mi na całym świecie, choć w naszym przy­pad­ku nie powin­no to mieć więk­szego znaczenia, staramy się wybier­ać lokaliza­cję najbliższą nam — w tym wypad­ku Frankfurt).

S3 zbu­dowane jest wokół kon­cepcji kubełków (buck­ets) — moż­na o nich myśleć jak o ‘dyskach’ w kom­put­erze — każdy plik musimy umieś­cić w wybranym kubełku, w ramach kubeł­ka nazwy plików nie mogą się powtarzać.

Nazwy plików zachowu­ją się nieco inaczej niż w przy­pad­ku dysków fizy­cznych — w przy­pad­ku S3 nie ma stricte fold­erów, moż­na za to uży­wać ukośników w nazwach, co w inter­fe­jsie webowym jest właśnie reprezen­towane jako ‘fold­ery’.

Poza tym więk­szość kon­cep­tów się pokry­wa z tym, co znamy z pod­sta­wowej obsłu­gi komputera :)

Upload plików

Zajmi­jmy się na początek uploa­dem plików na ser­w­er. Dwa ele­men­ty mamy już gotowe: nasz kon­trol­er z poprzed­niej lekcji oraz doku­men­tację do API (w tym wypad­ku sko­rzys­tamy z tej konkret­nej pod­strony)

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 ter­az połączyć oba te frag­men­ty w jeden :)

Jed­na zmi­ana, którą będziemy musieli wykon­ać to uży­ta meto­da putO­b­ject — w powyższym kodzie uży­wamy metody, która przyj­mu­je plik na dysku. My sko­rzys­tamy z wer­sji, która obsługu­je stru­mie­nie, opisanej w doku­men­tacji JavaDoc (http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3Client.html).

Wyko­rzys­tamy też inny sposób uwierzytel­ni­a­nia — poda­jąc bezpośred­nio nasz access key oraz secret key za pomocą klasy AWS­Cre­den­tials.

Ponieważ w kon­trol­erze mamy dostęp do upload­owanego pliku jako tabl­i­cy bajtów, musimy wyko­rzys­tać jeszcze jed­ną klasę, żeby zamienić tą tablicę na Input­Stream (oczeki­wany przez API S3):

InputStream is = new ByteArrayInputStream(fileBytes);

Z naszych obu przykładów bierze­my więc frag­men­ty zaz­nac­zone na czer­wono i wkle­jamy 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());
        }
    }

Inny­mi słowy — pomi­jamy wszys­tkie ele­men­ty ‘dookoła’ (infor­ma­c­je wyp­isy­wane na kon­sole, póki co także blo­ki try-catch; uzu­pełn­imy je później) i przenosimy samą logikę.

Następ­nie dopa­sowu­je­my nazwy zmi­en­nych i uzu­peł­ni­amy dodatkowy­mi oper­ac­ja­mi, które potrze­bu­je­my żeby dopa­sować dane w for­ma­cie który mamy, do tego akcep­towanego przez API (np. przy­toc­zonym wcześniej fragmentem).

W ostat­nim kroku uzu­peł­ni­amy wszys­tkie blo­ki try-catch (tutaj najwygod­niej poz­wolić po pros­tu Eclipse zro­bić to za nas i sko­rzys­tać z pod­powiadanych rozwiązań). W efek­cie otrzy­mu­je­my 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łu­gi wyjątków — są one już łapane przez nasz try-catch, który mieliśmy wcześniej.

Podsumowanie

W dzisiejszej lekcji nauczyliśmy się korzys­tać z API do umieszcza­nia plików na ser­w­erze. W kole­jnej częś­ci tej lekcji nauczymy się także pobier­ać pli­ki z ser­w­era i wyświ­et­lać je użytkown­ikom. Powiemy sobie też więcej o tym, gdzie szukać mate­ri­ałów jeśli mamy problem.

Zadanie

Zmody­fikuj aplikację tak, aby przesyłane przez użytkown­ików zdję­cia były zapisy­wane nie na dysku, ale na Ama­zon S3 (lub innym, wybranym przez Ciebie ser­w­erze plików).

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przy­dat­ną, mamy małą prośbę: pol­ub nasz fan­page. Dzię­ki temu będziesz zawsze na bieżą­co z nowy­mi treś­ci­a­mi na blogu ( i oczy­wiś­cie, z nowy­mi częś­ci­a­mi kur­su Javy). Dzięki!