Upload with new API
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Armin Friedl 2020-07-19 23:45:22 +02:00
parent 3ae0e912b7
commit 3860ed9177
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
13 changed files with 2643 additions and 4452 deletions

View file

@ -23,6 +23,12 @@ GET http://localhost:8080/api/fling
Content-Type: application/json Content-Type: application/json
:token :token
# Add a new fling
POST http://localhost:8080/api/fling
Content-Type: application/json
:token
{"name": "Shared Fling from querysheet", "expirationClicks": 12, "shared": true}
# 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

@ -9,6 +9,7 @@ import javax.persistence.GeneratedValue;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.persistence.Version; import javax.persistence.Version;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.annotations.UpdateTimestamp;
@ -16,7 +17,7 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@Entity @Entity
@Table(name = "Artifact") @Table(name = "Artifact", uniqueConstraints = @UniqueConstraint(columnNames = {"fling_id", "path"}))
@Getter @Getter
@Setter @Setter
public class ArtifactEntity { public class ArtifactEntity {

View file

@ -11,6 +11,6 @@ public interface FlingRepository extends JpaRepository<FlingEntity, UUID> {
FlingEntity findByShareId(String shareId); FlingEntity findByShareId(String shareId);
@Query("SELECT fe FROM FlingEntity fe JOIN ArtifactEntity ae ON fe.id=ae.id WHERE ae.id=:artifactId") @Query("SELECT fe FROM FlingEntity fe JOIN ArtifactEntity ae ON fe.id=ae.fling.id WHERE ae.id=:artifactId")
FlingEntity findByArtifactId(UUID artifactId); FlingEntity findByArtifactId(UUID artifactId);
} }

View file

@ -109,15 +109,15 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/***************************************/ /***************************************/
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/artifacts/{artifactId}/**") .antMatchers(HttpMethod.GET, "/api/artifacts/{artifactId}/**")
.access("@authorizationService.allowArtifactAccess(#artifactId, token)") .access("@authorizationService.allowArtifactAccess(#artifactId, authentication)")
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/artifacts/{artifactId}/data") .antMatchers(HttpMethod.POST, "/api/artifacts/{artifactId}/data")
.access("@authorizationService.allowArtifactUpload(#artifactId, token)") .access("@authorizationService.allowArtifactUpload(#artifactId, authentication)")
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/api/artifacts/{artifactId}") .antMatchers(HttpMethod.DELETE, "/api/artifacts/{artifactId}")
.access("@authorizationService.allowArtifactUpload(#artifactId, token)"); .access("@authorizationService.allowArtifactUpload(#artifactId, authentication)");
//@formatter:on //@formatter:on
} }

View file

@ -56,7 +56,8 @@ public class ArtifactService {
*/ */
public ArtifactDto create(UUID flingId, ArtifactDto artifactDto) { public ArtifactDto create(UUID flingId, ArtifactDto artifactDto) {
FlingEntity flingEntity = flingRepository.getOne(flingId); FlingEntity flingEntity = flingRepository.getOne(flingId);
log.debug("Creating new ArtifactEntity for ArtifactDto[.path={}]", artifactDto.getPath());
ArtifactEntity artifactEntity = artifactMapper.map(artifactDto); ArtifactEntity artifactEntity = artifactMapper.map(artifactDto);
artifactEntity.setFling(flingEntity); artifactEntity.setFling(flingEntity);
artifactEntity = artifactRepository.save(artifactEntity); artifactEntity = artifactRepository.save(artifactEntity);

View file

@ -89,14 +89,16 @@ public class FileSystemArchive implements ArchiveService {
public void storeArtifact(UUID artifactId, InputStream artifactStream) throws IOException { public void storeArtifact(UUID artifactId, InputStream artifactStream) throws IOException {
log.debug("Storing artifact {}", artifactId); log.debug("Storing artifact {}", artifactId);
setArchived(artifactId, false); synchronized (filesystems) {
FileSystem zipDisk = getZipDisk(artifactId); setArchived(artifactId, false);
Files.copy(artifactStream, getZipDiskPath(artifactId, zipDisk), FileSystem zipDisk = getZipDisk(artifactId);
StandardCopyOption.REPLACE_EXISTING); Files.copy(artifactStream, getZipDiskPath(artifactId, zipDisk),
StandardCopyOption.REPLACE_EXISTING);
// we need to close the zipDisk in order to flush it to disk // we need to close the zipDisk in order to flush it to disk
closeZipDisk(artifactId); closeZipDisk(artifactId);
setArchived(artifactId, true); setArchived(artifactId, true);
}
} }
@Override @Override
@ -129,7 +131,7 @@ public class FileSystemArchive implements ArchiveService {
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip"); Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
log.debug("Deleting fling [.id={}] at {}", flingId, zipDiskPath); log.debug("Deleting fling [.id={}] at {}", flingId, zipDiskPath);
if(Files.exists(zipDiskPath)) { if (Files.exists(zipDiskPath)) {
Files.delete(zipDiskPath); Files.delete(zipDiskPath);
} else { } else {
log.warn("No fling disk found at {}", zipDiskPath); log.warn("No fling disk found at {}", zipDiskPath);
@ -142,7 +144,6 @@ public class FileSystemArchive implements ArchiveService {
private void setArchived(UUID artifactId, boolean archived) { private void setArchived(UUID artifactId, boolean archived) {
ArtifactEntity artifactEntity = artifactRepository.getOne(artifactId); ArtifactEntity artifactEntity = artifactRepository.getOne(artifactId);
artifactEntity.setArchived(archived); artifactEntity.setArchived(archived);
log.debug("Artifact[.id={}] set to {} archived", artifactId, archived ? "" : "not"); log.debug("Artifact[.id={}] set to {} archived", artifactId, archived ? "" : "not");
} }

File diff suppressed because it is too large Load diff

View file

@ -1,42 +1,65 @@
import log from 'loglevel'; import log from 'loglevel';
import React from 'react'; import React from 'react';
import {Switch, Route, useLocation, Link} from "react-router-dom"; import { Switch, Route, useLocation, Link } from "react-router-dom";
import { useSelector } from "react-redux";
import FlingArtifacts from './FlingArtifacts'; import FlingArtifacts from './FlingArtifacts';
import Upload from './Upload'; import Upload from './Upload';
import Settings from './Settings'; import Settings from './Settings';
export default function FlingContent(props) { export default function FlingContent() {
let location = useLocation(); const location = useLocation();
const activeFling = useSelector(state => state.flings.activeFling);
function Empty() {
return (
<div className="empty">
<div className="empty-icon">
<i className="icon icon-search icon-2x"></i>
</div>
<p className="empty-title h5">No Fling selected</p>
<p className="empty-subtitle">Select a fling from the list</p>
</div>
);
}
function Content() {
function path(tail) { function path(tail) {
return `/admin/${props.activeFling}/${tail}`; return `/admin/${activeFling.id}/${tail}`;
} }
return( return (
<div className="fling-content p-2"> <div className="fling-content p-2">
{log.info("FlingContent location ", location)} {log.info("FlingContent location ", location)}
{log.info("FlingContent active fling ", props.activeFling)} {log.info("FlingContent active fling ", activeFling)}
<ul className="tab tab-block mt-0"> <ul className="tab tab-block mt-0">
<li className={`tab-item ${location.pathname !== path("upload") && location.pathname !== path("settings") ? "active": ""}`}> <li className={`tab-item ${location.pathname !== path("upload") && location.pathname !== path("settings") ? "active" : ""}`}>
<Link to={path("files")}>Files</Link> <Link to={path("files")}>Files</Link>
</li> </li>
<li className={`tab-item ${location.pathname === path("upload") ? "active": ""}`}> <li className={`tab-item ${location.pathname === path("upload") ? "active" : ""}`}>
<Link to={path("upload")}>Upload</Link> <Link to={path("upload")}>Upload</Link>
</li> </li>
<li className={`tab-item ${location.pathname === path("settings") ? "active": ""}`}> <li className={`tab-item ${location.pathname === path("settings") ? "active" : ""}`}>
<Link to={path("settings")}>Settings</Link> <Link to={path("settings")}>Settings</Link>
</li> </li>
</ul> </ul>
<div className="mt-2"> <div className="mt-2">
<Switch> <Switch>
<Route exact path="/admin/:fling"><FlingArtifacts activeFling={props.activeFling} /></Route> <Route exact path="/admin/:fling"><FlingArtifacts /></Route>
<Route path="/admin/:fling/files"><FlingArtifacts activeFling={props.activeFling} /></Route> <Route path="/admin/:fling/files"><FlingArtifacts /></Route>
<Route path="/admin/:fling/upload"><Upload activeFling={props.activeFling} /></Route> <Route path="/admin/:fling/upload"><Upload /></Route>
<Route path="/admin/:fling/settings"><Settings activeFling={props.activeFling} /></Route> <Route path="/admin/:fling/settings"><Settings /></Route>
</Switch> </Switch>
</div>
</div> </div>
</div>
); );
}
return (
<>
{ activeFling ? Content() : Empty() }
</>
);
} }

View file

@ -43,7 +43,7 @@ function TileAction(props) {
<li className="menu-item"> <li className="menu-item">
<div className="form-group"> <div className="form-group">
<label className="form-switch"> <label className="form-switch">
<input type="checkbox" checked={props.fling.shared} /> <input type="checkbox" disabled checked={props.fling.shared} />
<i className="form-icon" /> <i className="form-icon" />
{props.fling.shared ? "Shared" : "Private"} {props.fling.shared ? "Shared" : "Private"}
</label> </label>

View file

@ -1,5 +1,5 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState} from 'react'; import React, {useState, useEffect} from 'react';
import {useHistory, useLocation} from 'react-router-dom'; import {useHistory, useLocation} from 'react-router-dom';
import {fc, AuthClient} from '../../util/fc'; import {fc, AuthClient} from '../../util/fc';
@ -11,6 +11,25 @@ export default function Login() {
const location = useLocation(); const location = useLocation();
const { from } = location.state || { from: { pathname: "/admin" } }; const { from } = location.state || { from: { pathname: "/admin" } };
useEffect(() => {
sessionStorage.removeItem("token")
});
function handleSubmit(ev) {
ev.preventDefault();
let authClient = new AuthClient();
let opt = {adminAuth: new fc.AdminAuth(username, password)};
authClient.authenticateOwner(opt)
.then(response => {
log.info("Login successful");
sessionStorage.setItem('token', response);
log.debug("Returning back to", from);
history.replace(from);
}).catch(log.error);
};
return ( return (
<div className="container-center"> <div className="container-center">
<div> <div>
@ -41,19 +60,4 @@ export default function Login() {
</div> </div>
); );
function handleSubmit(ev) {
ev.preventDefault();
let authClient = new AuthClient();
let opt = {adminAuth: new fc.AdminAuth(username, password)};
authClient.authenticateOwner(opt)
.then(response => {
log.info("Login successful");
sessionStorage.setItem('token', response);
log.debug("Returning back to", from);
history.replace(from);
}).catch(log.error);
};
} }

View file

@ -1,217 +1,208 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState, useEffect, useRef} from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useSelector } from "react-redux";
import {artifactClient} from '../../util/flingclient'; import { ArtifactClient, FlingClient, fc } from '../../util/fc';
import { prettifyBytes, prettifyTimestamp } from '../../util/fn';
import upload from '../resources/upload.svg'; import upload from '../resources/upload.svg';
import drop from '../resources/drop.svg'; import drop from '../resources/drop.svg';
export default function Upload() {
let fileInputRef = useRef(null);
let [files, setFiles] = useState([]);
let [dragging, setDragging] = useState(false);
let [dragCount, setDragCount] = useState(0);
export default function Upload(props) { const activeFling = useSelector(state => state.flings.activeFling);
let fileInputRef = useRef(null);
let [files, setFiles] = useState([]);
let [dragging, setDragging] = useState(false);
let [dragCount, setDragCount] = useState(0);
useEffect(() => { useEffect(() => {
// prevent browser from trying to open the file when drag event // prevent browser from trying to open the file when drag event not
// not recognized properly // recognized properly
window.addEventListener("dragover",function(e){ window.addEventListener("dragover", e => e.preventDefault(), false);
e.preventDefault(); window.addEventListener("drop", e => e.preventDefault(), false);
},false); });
window.addEventListener("drop",function(e){
e.preventDefault();
},false);
});
function fileList() { function fileList() {
function readableBytes(bytes) { let fileList = [];
if(bytes <= 0) return "0 KB"; files.forEach((file, idx) => {
if (!file.uploaded) {
var i = Math.floor(Math.log(bytes) / Math.log(1024)), fileList.push(
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; <div key={idx} className="column col-6 col-md-12 mb-2">
<div className="card">
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i]; <div className="card-header">
} <i className="icon icon-cross float-right c-hand"
onClick={ev => deleteFile(idx)} />
let fileList = []; <div className="card-title h5">{file.name}</div>
files.forEach((file,idx) => { <div className="card-subtitle text-gray">
if(!file.uploaded) { {`${prettifyTimestamp(file.lastModified)}, ` +
fileList.push( `${prettifyBytes(file.size)}`}
<div className="column col-6 col-md-12 mb-2">
<div className="card">
<div className="card-header">
<i className="icon icon-cross float-right c-hand" onClick={ev => deleteFile(idx)}/>
<div className="card-title h5">{file.name}</div>
<div className="card-subtitle text-gray">{(new Date(file.lastModified)).toLocaleString()+", "+readableBytes(file.size)}</div>
</div>
</div>
</div>
);
}
});
return fileList;
}
function deleteFile(idx) {
let f = [...files];
f.splice(idx, 1);
setFiles(f);
}
function totalSize() {
function readableBytes(bytes) {
if(bytes <= 0) return "0 KB";
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
}
let totalSize = 0;
for(let file of files) {
totalSize += file.size;
}
return readableBytes(totalSize);
}
function handleClick(ev) {
fileInputRef.current.click();
}
function handleFileInputChange(ev) {
let fileInputFiles = fileInputRef.current.files;
if (!fileInputFiles) {
console.warn("No files selected");
return;
}
setFiles([...files, ...fileInputFiles]);
}
function handleDrop(ev) {
stopEvent(ev);
ev.persist();
let evFiles = ev.dataTransfer.files;
if (!evFiles) {
console.warn("Dropzone triggered without files");
return;
}
setFiles([...files, ...fileListToArray(evFiles)]);
setDragging(false);
setDragCount(0);
}
function fileListToArray(fileList) {
if(fileList === undefined || fileList === null) {
return [];
}
let arr = [];
for (let i=0; i<fileList.length; i++) { arr.push(fileList[i]); }
return arr;
}
function handleOnDragEnter(ev) {
stopEvent(ev);
if(dragCount === 0) setDragging(true);
setDragCount(dragCount+1);
}
function handleOnDragLeave(ev) {
stopEvent(ev);
let dc = dragCount;
dc -= 1;
setDragCount(dc);
if(dc === 0) setDragging(false);
}
function stopEvent(ev) {
ev.preventDefault();
ev.stopPropagation();
}
function logFiles() {
log.info("Files so far: ["+files.map((i) => i.name).join(',')+"]");
}
function setFileUploaded(idx) {
let f = [...files];
f[idx].uploaded = true;
setFiles(f);
}
function handleUpload() {
files.forEach((file, idx) => {
artifactClient.postArtifact(props.activeFling, file)
.then(response => {
setFileUploaded(idx);
});
});
}
function zoneContent(dragging) {
if(dragging){
return(
<>
<img className="dropzone-icon" alt="dropzone icon" src={drop} />
<h5 className="text-primary">Drop now!</h5>
</>
);
}else {
return(
<>
<img className="dropzone-icon-upload" alt="dropzone icon" src={upload} />
<h5>Click or Drop</h5>
</>
);
}
}
return(
<div className="container">
{logFiles()}
<div className="columns">
<div className="column col-4 col-sm-12">
<div className="dropzone c-hand py-2"
onDrop={handleDrop}
onClick={handleClick}
onDragOver={stopEvent}
onDragEnter={handleOnDragEnter}
onDragLeave={handleOnDragLeave}>
<input className="d-hide" ref={fileInputRef} type="file" multiple onChange={handleFileInputChange} />
{zoneContent(dragging)}
</div>
</div>
<div className="column col-8 col-sm-12" >
<div className="file-list">
<div className="row">
<div className="container">
<div className="columns">
{fileList()}
</div>
</div>
</div>
<div className="upload-command-line m-2">
<span className="total-upload">Total Size: {totalSize()}</span>
<button className="btn btn-primary btn-upload" onClick={handleUpload}>Upload</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
);
}
});
return fileList;
}
function deleteFile(idx) {
let f = [...files];
f.splice(idx, 1);
setFiles(f);
}
function totalSize() {
let totalSize = 0;
for (let file of files) {
totalSize += file.size;
}
return prettifyBytes(totalSize);
}
function handleClick(ev) {
fileInputRef.current.click();
}
function handleFileInputChange(ev) {
let fileInputFiles = fileInputRef.current.files;
if (!fileInputFiles) {
console.warn("No files selected");
return;
}
setFiles([...files, ...fileInputFiles]);
}
function handleDrop(ev) {
stopEvent(ev);
ev.persist();
let evFiles = ev.dataTransfer.files;
if (!evFiles) {
console.warn("Dropzone triggered without files");
return;
}
setFiles([...files, ...fileListToArray(evFiles)]);
setDragging(false);
setDragCount(0);
}
function fileListToArray(fileList) {
if (fileList === undefined || fileList === null) {
return [];
}
let arr = [];
for (let i = 0; i < fileList.length; i++) { arr.push(fileList[i]); }
return arr;
}
function handleOnDragEnter(ev) {
stopEvent(ev);
if (dragCount === 0) setDragging(true);
setDragCount(dragCount + 1);
}
function handleOnDragLeave(ev) {
stopEvent(ev);
let dc = dragCount;
dc -= 1;
setDragCount(dc);
if (dc === 0) setDragging(false);
}
function stopEvent(ev) {
ev.preventDefault();
ev.stopPropagation();
}
function logFiles() {
log.info("Files so far: [" + files.map((i) => i.name).join(',') + "]");
}
function setFileUploaded(idx) {
let f = [...files];
f[idx].uploaded = true;
setFiles(f);
}
function handleUpload() {
const flingClient = new FlingClient();
const artifactClient = new ArtifactClient();
files.forEach((file, idx) => {
let artifact = new fc.Artifact(file.name)
flingClient.postArtifact(activeFling.id, { artifact: artifact })
.then(artifact => {
artifactClient.uploadArtifactData(artifact.id, { body: file });
setFileUploaded(idx);
});
});
}
function zoneContent(dragging) {
if (dragging) {
return (
<>
<img className="dropzone-icon" alt="dropzone icon" src={drop} />
<h5 className="text-primary">Drop now!</h5>
</>
);
} else {
return (
<>
<img className="dropzone-icon-upload" alt="dropzone icon" src={upload} />
<h5>Click or Drop</h5>
</>
);
}
}
return (
<div className="container">
{logFiles()}
<div className="columns">
<div className="column col-4 col-sm-12">
<div className="dropzone c-hand py-2"
onDrop={handleDrop}
onClick={handleClick}
onDragOver={stopEvent}
onDragEnter={handleOnDragEnter}
onDragLeave={handleOnDragLeave}>
<input className="d-hide" ref={fileInputRef} type="file" multiple onChange={handleFileInputChange} />
{zoneContent(dragging)}
</div>
</div> </div>
);
<div className="column col-8 col-sm-12" >
<div className="file-list">
<div className="row">
<div className="container">
<div className="columns">
{fileList()}
</div>
</div>
</div>
<div className="upload-command-line m-2">
<span className="total-upload">Total Size: {totalSize()}</span>
<button className="btn btn-primary btn-upload" onClick={handleUpload}>Upload</button>
</div>
</div>
</div>
</div>
</div>
);
} }

View file

@ -0,0 +1,39 @@
import log from "loglevel";
import produce from "immer";
import { SET_FLINGS, SET_ACTIVE_FLING, ADD_FLING } from "../actionTypes";
const initialState = {
// type [fc.Artifact]
aritfacts: []
}
export default produce((draft, action) => {
switch (action.type) {
case SET_FLINGS:
draft.flings = action.payload;
break;
case ADD_FLING:
// Check storage again here, otherwise there could be a race
// condition due to async calls of SET_FLINGS and ADD_FLING
let foundFlingIdx = draft.flings.findIndex(fling =>
fling.id === action.payload.id);
if (foundFlingIdx === -1) {
log.debug(`Adding new fling with id ${action.payload.id}`)
draft.flings.push(action.payload);
} else {
log.debug(`Fling already exists. ` +
`Updating fling with id ${action.payload.id}`)
draft.flings[foundFlingIdx] = action.payload
}
break;
case SET_ACTIVE_FLING:
draft.activeFling = action.payload;
break;
default:
break;
}
return draft;
}, initialState);

19
web/fling/src/util/fn.js Normal file
View file

@ -0,0 +1,19 @@
/*
* Returns a human readable presentation of `bytes`
*/
export function prettifyBytes(bytes) {
if (bytes <= 0) return "0 KB";
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
}
/**
* Returns a human readable date for a unix timestamp in milliseconds
*/
export function prettifyTimestamp(timestamp, withTime=false) {
let date = new Date(timestamp);
return withTime ? date.toLocaleString(): date.toLocaleDateString();
}