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"}
-> 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

View file

@ -101,11 +101,14 @@ public class FlingController {
public ResponseEntity<Resource> 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);
}

View file

@ -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;

View file

@ -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))

View file

@ -26,7 +26,7 @@ function FlingArtifactControl(props) {
log.trace(`Generated download url: ${url}`);
frame.src = url;
iframeContainer.current.appendChild(frame);
})
});
}
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) {
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(
<div>
<div className="container-center">
<div className="card direct-download-card">
<div className="card-body ">
{packaging
? <><div className="loading loading-lg" />
{waitingMessage ? waitingMessage: "Packaging up your files..."}
</>
: <>
<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/><a href={downloadUrl}>Click here</a></span>
</>
}
</div>
</div>
function invalidateLink(ev) {
setDone(true);
window.location.href = downloadUrl;
}
function reloadPage(ev) {
window.location.reload();
}
return (
<div>
<div className="container-center">
<div className="card direct-download-card">
<div className="card-body ">
{packaging
? <><div className="loading loading-lg" />
{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 className="d-hide" ref={iframeContainer} />
</div>
);
</div>
<div className="d-hide" ref={iframeContainer} />
</div>
);
}

View file

@ -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 (
<div>
{fling.sharing && fling.sharing.directDownload
? <DirectDownload fling={fling} />
: <FlingUserList fling={fling} />}
</div>
<>
{loading
? <div></div>
: <div>
{fling.shared && fling.directDownload
? <DirectDownload fling={fling} />
: <FlingUserList fling={fling} />}
</div>
}
</>
);
}

View file

@ -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 => {