Use new API in DirectDownload, improve redirection
All checks were successful
continuous-integration/drone/push Build is passing

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.
This commit is contained in:
Armin Friedl 2020-07-26 01:33:32 +02:00
parent 0824d8367f
commit e9ad499850
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
8 changed files with 119 additions and 59 deletions

View file

@ -16,13 +16,30 @@ Content-Type: application/json
{"adminName": "admin", "adminPassword":"123"} {"adminName": "admin", "adminPassword":"123"}
-> run-hook (restclient-set-var ":token" (buffer-substring-no-properties 1 (line-end-position))) -> 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 all flings
GET http://localhost:8080/api/fling GET http://localhost:8080/api/fling
Content-Type: application/json Content-Type: application/json
:token :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 # Add a new fling
POST http://localhost:8080/api/fling POST http://localhost:8080/api/fling
Content-Type: application/json Content-Type: application/json

View file

@ -101,11 +101,14 @@ public class FlingController {
public ResponseEntity<Resource> getFlingData(@PathVariable UUID id) throws IOException { public ResponseEntity<Resource> getFlingData(@PathVariable UUID id) throws IOException {
FlingDto flingDto = flingService.getById(id); FlingDto flingDto = flingService.getById(id);
InputStreamResource data = new InputStreamResource(archiveService.getFling(id)); InputStreamResource data = new InputStreamResource(archiveService.getFling(id));
Long length = data.contentLength();
data = new InputStreamResource(archiveService.getFling(id));
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
"attachment;filename=\"" + flingDto.getName() + ".zip" + "\"") "attachment;filename=\"" + flingDto.getName() + ".zip" + "\"")
.contentLength(200L) // FIXME .contentLength(length)
.contentType(MediaType.APPLICATION_OCTET_STREAM) .contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(data); .body(data);
} }

View file

@ -22,7 +22,6 @@ import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.model.dto.AdminAuthDto; 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.model.dto.UserAuthDto;
import net.friedl.fling.persistence.entities.FlingEntity; import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.entities.TokenEntity; import net.friedl.fling.persistence.entities.TokenEntity;

View file

@ -122,7 +122,7 @@ public class FlingControllerTest {
@Test @Test
public void replaceFling_ok() throws Exception { public void replaceFling_ok() throws Exception {
FlingDto flingDto = new FlingDto(flingId, "new-name", Instant.EPOCH, "shareId", "new-authCode", 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) mockMvc.perform(put("/api/fling/{id}", flingId)
.content(mapper.writeValueAsString(flingDto)) .content(mapper.writeValueAsString(flingDto))
@ -275,7 +275,11 @@ public class FlingControllerTest {
byte[] testZip = new byte[testZipInt.length]; byte[] testZip = new byte[testZipInt.length];
for (int idx = 0; idx < testZip.length; idx++) testZip[idx] = (byte) testZipInt[idx]; 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)) mockMvc.perform(get("/api/fling/{id}/data", flingId))
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))

View file

@ -26,7 +26,7 @@ function FlingArtifactControl(props) {
log.trace(`Generated download url: ${url}`); log.trace(`Generated download url: ${url}`);
frame.src = url; frame.src = url;
iframeContainer.current.appendChild(frame); iframeContainer.current.appendChild(frame);
}) });
} }
return ( return (

View file

@ -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) { export default function FlingUser(props) {
let iframeContainer = useRef(null); let iframeContainer = useRef(null);
let [packaging, setPackaging] = useState(true); let [packaging, setPackaging] = useState(true);
let [waitingMessage, setWaitingMessage] = useState(""); let [done, setDone] = useState(false);
let [downloadUrl, setDownloadUrl] = useState(""); let [waitingMessage, setWaitingMessage] = useState("");
let [downloadUrl, setDownloadUrl] = useState("");
useEffect(handleDownload, []); useEffect(handleDownload, []);
function handleDownload() { function handleDownload() {
flingClient.packageFling(props.fling.id) let authClient = new AuthClient();
.then(downloadUrl => { authClient.deriveToken({ singleUse: true })
setPackaging(false); .then(token => {
// We need this iframe hack because with a regular href, while let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/fling/${props.fling.id}/data?derivedToken=${token}`;
// the browser downloads the file fine, it also reloads the page, hence log.trace(`Generated download url for link: ${url}`);
// loosing all logs and state setDownloadUrl(url);
let frame = document.createElement("iframe"); })
frame.src = downloadUrl; .then(
iframeContainer.current.appendChild(frame); authClient.deriveToken({ singleUse: true })
setDownloadUrl(downloadUrl); .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...", let randMsg = ["Please stay patient...",
"Your download will be ready soon...", "Your download will be ready soon...",
"Packaging up your files...", "Packaging up your files...",
"Almost there..."]; "Almost there..."];
setInterval(() => setWaitingMessage(randMsg[Math.floor(Math.random() * randMsg.length)]), 10000); setInterval(() => setWaitingMessage(randMsg[Math.floor(Math.random() * randMsg.length)]), 10000);
} }
return( function invalidateLink(ev) {
<div> setDone(true);
<div className="container-center"> window.location.href = downloadUrl;
<div className="card direct-download-card"> }
<div className="card-body ">
{packaging function reloadPage(ev) {
? <><div className="loading loading-lg" /> window.location.reload();
{waitingMessage ? waitingMessage: "Packaging up your files..."} }
</>
: <> return (
<h5>Your download is <span className="text-primary">ready!</span></h5> <div>
<i className="icon icon-check icon-2x text-primary" /><br/> <div className="container-center">
<span className="text-dark">Download doesn't start? <br/><a href={downloadUrl}>Click here</a></span> <div className="card direct-download-card">
</> <div className="card-body ">
} {packaging
</div> ? <><div className="loading loading-lg" />
</div> {waitingMessage ? waitingMessage : "Packaging up your files..."}
</>
: !done
? <>
<h5>Your download is <span className="text-primary">ready!</span></h5>
<i className="icon icon-check icon-2x text-primary" /><br />
<span className="text-dark">Download doesn't start? <br />
<button className="btn btn-link" onClick={invalidateLink}>Click here</button></span>
</>
: <>
<h5>Thanks for <span className="text-primary">downloading!</span></h5>
<i className="icon icon-check icon-2x text-primary" /><br />
<span className="text-dark">Want to download again? <br />
<button className="btn btn-link" onClick={reloadPage}>Reload page</button></span>
</>
}
</div> </div>
<div className="d-hide" ref={iframeContainer} />
</div> </div>
); </div>
<div className="d-hide" ref={iframeContainer} />
</div>
);
} }

View file

@ -10,18 +10,27 @@ import FlingUserList from './FlingUserList';
export default function FlingUser() { export default function FlingUser() {
let { shareId } = useParams(); let { shareId } = useParams();
let [fling, setFling] = useState({}); let [fling, setFling] = useState({});
let [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
let flingClient = new FlingClient(); let flingClient = new FlingClient();
flingClient.getFlingByShareId(shareId) flingClient.getFlingByShareId(shareId)
.then(f => setFling(f)); .then(f => {
setFling(f);
setLoading(false);
});
}, [shareId]); }, [shareId]);
return ( return (
<div> <>
{fling.sharing && fling.sharing.directDownload {loading
? <DirectDownload fling={fling} /> ? <div></div>
: <FlingUserList fling={fling} />} : <div>
</div> {fling.shared && fling.directDownload
? <DirectDownload fling={fling} />
: <FlingUserList fling={fling} />}
</div>
}
</>
); );
} }

View file

@ -12,7 +12,7 @@ export default function Unlock() {
useEffect(() => { useEffect(() => {
let authClient = new AuthClient(); let authClient = new AuthClient();
let userAuth = new fc.UserAuth(location.state.shareId, "") let userAuth = new fc.UserAuth(location.state.shareId, "");
authClient.authenticateUser({ 'userAuth': userAuth }) authClient.authenticateUser({ 'userAuth': userAuth })
.then(response => { .then(response => {
@ -20,7 +20,7 @@ export default function Unlock() {
sessionStorage.setItem('token', response); sessionStorage.setItem('token', response);
history.replace(location.state.from); history.replace(location.state.from);
}).catch(error => { }).catch(error => {
log.info("Fling protected. Could not unlock without code.") log.info("Fling protected. Could not unlock without code.");
}); });
}, [location, history]); }, [location, history]);
@ -46,7 +46,7 @@ export default function Unlock() {
function handleSubmit(ev) { function handleSubmit(ev) {
ev.preventDefault(); ev.preventDefault();
let authClient = new AuthClient(); let authClient = new AuthClient();
let userAuth = new fc.UserAuth(shareId, authCode) let userAuth = new fc.UserAuth(shareId, authCode);
authClient.authenticateUser({ 'userAuth': userAuth }) authClient.authenticateUser({ 'userAuth': userAuth })
.then(response => { .then(response => {