From e9ad499850ad99bbcf76d773e927a555892647c9 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sun, 26 Jul 2020 01:33:32 +0200 Subject: [PATCH] Use new API in DirectDownload, improve redirection DirectDownload now uses the new Fling API, including one-time tokens for download. When accessing a fling, while loading the fling, instead of showing the ArtifactList an empty page is shown. This way the redirection does not leak through so visibly when using direct download and the loading of the fling takes a bit longer. --- examples/querysheet.http | 19 ++- .../fling/controller/FlingController.java | 5 +- .../fling/service/AuthenticationService.java | 1 - .../fling/controller/FlingControllerTest.java | 8 +- .../src/components/admin/FlingArtifacts.jsx | 2 +- .../src/components/user/DirectDownload.jsx | 116 +++++++++++------- web/fling/src/components/user/FlingUser.jsx | 21 +++- web/fling/src/components/user/Unlock.jsx | 6 +- 8 files changed, 119 insertions(+), 59 deletions(-) diff --git a/examples/querysheet.http b/examples/querysheet.http index 5e08215..5995cff 100644 --- a/examples/querysheet.http +++ b/examples/querysheet.http @@ -16,13 +16,30 @@ Content-Type: application/json {"adminName": "admin", "adminPassword":"123"} -> run-hook (restclient-set-var ":token" (buffer-substring-no-properties 1 (line-end-position))) -:token = Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTUxNzY4OTksImV4cCI6MTU5NTM1Njg5OSwic3ViIjoiYWRtaW4ifQ.uRh_xBCrBiLQEBah9I8bYWM-Zph-V_pzQVdaGSU5Mlc +:token = Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTU2NzUxMjAsImV4cCI6MTU5NTg1NTEyMCwic3ViIjoiYWRtaW4ifQ.WzrGTTZTYHYOw8SskHQ2_sob2tzLIF6q8y8_2oyuafs # Get all flings GET http://localhost:8080/api/fling Content-Type: application/json :token +:flingid = 9f7353a3-efaa-41af-9f93-61e02dc5e440 + +# Put a fling +PUT http://localhost:8080/api/fling/:flingid +Content-Type: application/json +:token + { + "id": "9f7353a3-efaa-41af-9f93-61e02dc5e440", + "name": "Shared Fling from querysheetsdfasfd", + "creationTime": 1595253659362, + "shareId": "WWgTlNZJPZDQ6oowUYfxcQqq", + "directDownload": false, + "allowUpload": false, + "shared": true, + "expirationClicks": 12 + } + # Add a new fling POST http://localhost:8080/api/fling Content-Type: application/json diff --git a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java index db4dddc..584b248 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java @@ -101,11 +101,14 @@ public class FlingController { public ResponseEntity getFlingData(@PathVariable UUID id) throws IOException { FlingDto flingDto = flingService.getById(id); InputStreamResource data = new InputStreamResource(archiveService.getFling(id)); + Long length = data.contentLength(); + + data = new InputStreamResource(archiveService.getFling(id)); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + flingDto.getName() + ".zip" + "\"") - .contentLength(200L) // FIXME + .contentLength(length) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(data); } diff --git a/service/fling/src/main/java/net/friedl/fling/service/AuthenticationService.java b/service/fling/src/main/java/net/friedl/fling/service/AuthenticationService.java index 2617716..9849d3b 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/AuthenticationService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/AuthenticationService.java @@ -22,7 +22,6 @@ import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; import net.friedl.fling.model.dto.AdminAuthDto; -import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.model.dto.UserAuthDto; import net.friedl.fling.persistence.entities.FlingEntity; import net.friedl.fling.persistence.entities.TokenEntity; diff --git a/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java b/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java index 22c95c6..22340ba 100644 --- a/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java +++ b/service/fling/src/test/java/net/friedl/fling/controller/FlingControllerTest.java @@ -122,7 +122,7 @@ public class FlingControllerTest { @Test public void replaceFling_ok() throws Exception { FlingDto flingDto = new FlingDto(flingId, "new-name", Instant.EPOCH, "shareId", "new-authCode", - false, true, true, 1, null); + false, true, true, 1, null); mockMvc.perform(put("/api/fling/{id}", flingId) .content(mapper.writeValueAsString(flingDto)) @@ -275,7 +275,11 @@ public class FlingControllerTest { byte[] testZip = new byte[testZipInt.length]; for (int idx = 0; idx < testZip.length; idx++) testZip[idx] = (byte) testZipInt[idx]; - when(archiveService.getFling(any())).thenReturn(new ByteArrayInputStream(testZip)); + when(archiveService.getFling(any())) + .thenAnswer((invocation) -> { + // need to use thenAnswer here to always return a fresh new (unclosed) input stream + return new ByteArrayInputStream(testZip); + }); mockMvc.perform(get("/api/fling/{id}/data", flingId)) .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) diff --git a/web/fling/src/components/admin/FlingArtifacts.jsx b/web/fling/src/components/admin/FlingArtifacts.jsx index ebe2684..0e71e5b 100644 --- a/web/fling/src/components/admin/FlingArtifacts.jsx +++ b/web/fling/src/components/admin/FlingArtifacts.jsx @@ -26,7 +26,7 @@ function FlingArtifactControl(props) { log.trace(`Generated download url: ${url}`); frame.src = url; iframeContainer.current.appendChild(frame); - }) + }); } return ( diff --git a/web/fling/src/components/user/DirectDownload.jsx b/web/fling/src/components/user/DirectDownload.jsx index 1055018..9981a38 100644 --- a/web/fling/src/components/user/DirectDownload.jsx +++ b/web/fling/src/components/user/DirectDownload.jsx @@ -1,54 +1,82 @@ -import React, {useRef, useState, useEffect} from 'react'; +import log from 'loglevel'; +import React, { useRef, useState, useEffect } from 'react'; -import {flingClient} from '../../util/flingclient'; +import { AuthClient } from '../../util/fc'; export default function FlingUser(props) { - let iframeContainer = useRef(null); - let [packaging, setPackaging] = useState(true); - let [waitingMessage, setWaitingMessage] = useState(""); - let [downloadUrl, setDownloadUrl] = useState(""); + let iframeContainer = useRef(null); + let [packaging, setPackaging] = useState(true); + let [done, setDone] = useState(false); + let [waitingMessage, setWaitingMessage] = useState(""); + let [downloadUrl, setDownloadUrl] = useState(""); - useEffect(handleDownload, []); + useEffect(handleDownload, []); - function handleDownload() { - flingClient.packageFling(props.fling.id) - .then(downloadUrl => { - setPackaging(false); - // We need this iframe hack because with a regular href, while - // the browser downloads the file fine, it also reloads the page, hence - // loosing all logs and state - let frame = document.createElement("iframe"); - frame.src = downloadUrl; - iframeContainer.current.appendChild(frame); - setDownloadUrl(downloadUrl); - }); + function handleDownload() { + let authClient = new AuthClient(); + authClient.deriveToken({ singleUse: true }) + .then(token => { + let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/fling/${props.fling.id}/data?derivedToken=${token}`; + log.trace(`Generated download url for link: ${url}`); + setDownloadUrl(url); + }) + .then( + authClient.deriveToken({ singleUse: true }) + .then(token => { + setPackaging(false); + // We need this iframe hack because with a regular href, while + // the browser downloads the file fine, it also reloads the page, hence + // loosing all logs and state + let frame = document.createElement("iframe"); + let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/fling/${props.fling.id}/data?derivedToken=${token}`; + log.trace(`Generated download url: ${url}`); + frame.src = url; + iframeContainer.current.appendChild(frame); + })); - let randMsg = ["Please stay patient...", - "Your download will be ready soon...", - "Packaging up your files...", - "Almost there..."]; - setInterval(() => setWaitingMessage(randMsg[Math.floor(Math.random() * randMsg.length)]), 10000); - } + let randMsg = ["Please stay patient...", + "Your download will be ready soon...", + "Packaging up your files...", + "Almost there..."]; + setInterval(() => setWaitingMessage(randMsg[Math.floor(Math.random() * randMsg.length)]), 10000); + } - return( -
-
-
-
- {packaging - ? <>
- {waitingMessage ? waitingMessage: "Packaging up your files..."} - - : <> -
Your download is ready!
-
- Download doesn't start?
Click here
- - } -
-
+ function invalidateLink(ev) { + setDone(true); + window.location.href = downloadUrl; + } + + function reloadPage(ev) { + window.location.reload(); + } + + return ( +
+
+
+
+ {packaging + ? <>
+ {waitingMessage ? waitingMessage : "Packaging up your files..."} + + : !done + ? <> +
Your download is ready!
+
+ Download doesn't start?
+
+ + : <> +
Thanks for downloading!
+
+ Want to download again?
+
+ + }
-
- ); +
+
+
+ ); } diff --git a/web/fling/src/components/user/FlingUser.jsx b/web/fling/src/components/user/FlingUser.jsx index b76a37f..96bf1dd 100644 --- a/web/fling/src/components/user/FlingUser.jsx +++ b/web/fling/src/components/user/FlingUser.jsx @@ -10,18 +10,27 @@ import FlingUserList from './FlingUserList'; export default function FlingUser() { let { shareId } = useParams(); let [fling, setFling] = useState({}); + let [loading, setLoading] = useState(true); useEffect(() => { let flingClient = new FlingClient(); flingClient.getFlingByShareId(shareId) - .then(f => setFling(f)); + .then(f => { + setFling(f); + setLoading(false); + }); }, [shareId]); return ( -
- {fling.sharing && fling.sharing.directDownload - ? - : } -
+ <> + {loading + ?
+ :
+ {fling.shared && fling.directDownload + ? + : } +
+ } + ); } diff --git a/web/fling/src/components/user/Unlock.jsx b/web/fling/src/components/user/Unlock.jsx index 799be63..b30bfe0 100644 --- a/web/fling/src/components/user/Unlock.jsx +++ b/web/fling/src/components/user/Unlock.jsx @@ -12,7 +12,7 @@ export default function Unlock() { useEffect(() => { let authClient = new AuthClient(); - let userAuth = new fc.UserAuth(location.state.shareId, "") + let userAuth = new fc.UserAuth(location.state.shareId, ""); authClient.authenticateUser({ 'userAuth': userAuth }) .then(response => { @@ -20,7 +20,7 @@ export default function Unlock() { sessionStorage.setItem('token', response); history.replace(location.state.from); }).catch(error => { - log.info("Fling protected. Could not unlock without code.") + log.info("Fling protected. Could not unlock without code."); }); }, [location, history]); @@ -46,7 +46,7 @@ export default function Unlock() { function handleSubmit(ev) { ev.preventDefault(); let authClient = new AuthClient(); - let userAuth = new fc.UserAuth(shareId, authCode) + let userAuth = new fc.UserAuth(shareId, authCode); authClient.authenticateUser({ 'userAuth': userAuth }) .then(response => {