mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
feature (download): Add a way to download a file in the IDE + fix - #34
This commit is contained in:
parent
76172f0239
commit
3e2714fb33
14 changed files with 208 additions and 98 deletions
|
|
@ -92,9 +92,11 @@ const Logout = (props) => {
|
|||
|
||||
const Saving = (props) => {
|
||||
return (
|
||||
<NgIf className="component_saving" cond={props.needSaving === true}>
|
||||
<ReactCSSTransitionGroup transitionName="saving_indicator" transitionLeave={true} transitionEnter={true} transitionAppear={true} transitionLeaveTimeout={200} transitionEnterTimeout={500} transitionAppearTimeout={500}>
|
||||
<NgIf key={props.needSaving} className="component_saving" cond={props.needSaving === true}>
|
||||
*
|
||||
</NgIf>
|
||||
</ReactCSSTransitionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
width: 95%;
|
||||
max-width: 800px;
|
||||
padding: 0;
|
||||
span{display: block; padding: 7px 0;}
|
||||
> span{display: block; padding: 7px 0;}
|
||||
div, li{
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
@ -31,6 +31,9 @@
|
|||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.component_saving{
|
||||
padding-left: 1px;
|
||||
}
|
||||
|
||||
.component_path-element{
|
||||
display: inline-block;
|
||||
|
|
@ -128,4 +131,27 @@ body.touch-yes{
|
|||
transform: translateX(0);
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.saving_indicator-leave{
|
||||
opacity: 1;
|
||||
}
|
||||
.saving_indicator-leave.saving_indicator-leave-active{
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.saving_indicator-enter, .saving_indicator-appear{
|
||||
transform-origin: center;
|
||||
animation-name: bounce;
|
||||
animation-duration: 0.5s;
|
||||
@keyframes bounce {
|
||||
0% { transform: scale(0); }
|
||||
30% { transform: scale(1.5);}
|
||||
100% { transform: scale(1);}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
export function screenHeight(){
|
||||
const $breadcrumb = document.querySelector(".breadcrumb");
|
||||
const $breadcrumb = document.querySelector(".component_breadcrumb");
|
||||
const $menubar = document.querySelector(".component_menubar");
|
||||
let size = document.body.clientHeight;
|
||||
if($breadcrumb){
|
||||
size -= $breadcrumb.clientHeight;
|
||||
}
|
||||
if($breadcrumb){ size -= $breadcrumb.clientHeight; }
|
||||
if($menubar){ size -= $menubar.clientHeight; }
|
||||
return size;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,15 +101,27 @@ class FileSystem{
|
|||
cat(path){
|
||||
const url = '/api/files/cat?path='+prepare(path);
|
||||
return http_get(url, 'raw')
|
||||
.then((res) => cache.put(cache.FILE_CONTENT, path, {result: res}))
|
||||
.then((res) => {
|
||||
if(is_binary(res) === false) cache.put(cache.FILE_CONTENT, path, {result: res});
|
||||
return Promise.resolve(res);
|
||||
})
|
||||
.catch((res) => {
|
||||
return cache.get(cache.FILE_CONTENT, path)
|
||||
.then((_res) => {
|
||||
if(!_res || !_res.result) return Promise.reject(_res);
|
||||
if(!_res || !_res.result) return Promise.reject(res);
|
||||
return Promise.resolve(_res.result);
|
||||
})
|
||||
.catch(() => Promise.reject(res));
|
||||
})
|
||||
.then((res) => {
|
||||
if(is_binary(res) === true) return Promise.reject({code: 'BINARY_FILE'});
|
||||
return Promise.resolve(res);
|
||||
});
|
||||
|
||||
function is_binary(str){
|
||||
// Reference: https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character
|
||||
return /\ufffd/.test(str);
|
||||
}
|
||||
}
|
||||
url(path){
|
||||
const url = '/api/files/cat?path='+prepare(path);
|
||||
|
|
@ -181,7 +193,10 @@ class FileSystem{
|
|||
function update_from(){
|
||||
return cache.get(cache.FILE_PATH, dirname(from), false)
|
||||
.then((res_from) => {
|
||||
let _file = {name: basename(from), type: /\/$/.test(from) ? 'directory' : 'file'};
|
||||
let _file = {
|
||||
name: basename(from),
|
||||
type: /\/$/.test(from) ? 'directory' : 'file'
|
||||
};
|
||||
res_from.results = res_from.results.map((file) => {
|
||||
if(file.name === basename(from)){
|
||||
file.name = basename(to);
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ export class ViewerPage extends React.Component {
|
|||
super(props);
|
||||
this.state = {
|
||||
path: props.match.url.replace('/view', ''),
|
||||
url: null,
|
||||
filename: Path.basename(props.match.url.replace('/view', '')) || 'untitled.dat',
|
||||
opener: null,
|
||||
data: '',
|
||||
content: null,
|
||||
needSaving: false,
|
||||
isSaving: false,
|
||||
loading: true,
|
||||
|
|
@ -36,35 +37,38 @@ export class ViewerPage extends React.Component {
|
|||
}
|
||||
|
||||
componentWillMount(){
|
||||
this.setState({loading: null}, () => {
|
||||
window.setTimeout(() => {
|
||||
if(this.state.loading === null) this.setState({loading: true});
|
||||
}, 500);
|
||||
const metadata = () => {
|
||||
return new Promise((done, err) => {
|
||||
let app_opener = opener(this.state.path);
|
||||
Files.url(this.state.path).then((url) => {
|
||||
this.setState({
|
||||
url: url,
|
||||
opener: app_opener
|
||||
}, () => done(app_opener));
|
||||
}).catch(err => {
|
||||
notify.send(err, 'error');
|
||||
err(err);
|
||||
});
|
||||
let app = opener(this.state.path);
|
||||
});
|
||||
};
|
||||
const data_fetch = (app) => {
|
||||
if(app === 'editor'){
|
||||
Files.cat(this.state.path).then((content) => {
|
||||
this.setState({data: content, loading: false, opener: app});
|
||||
this.setState({content: content || "", loading: false});
|
||||
}).catch(err => {
|
||||
if(err && err.code === 'CANCELLED'){ return; }
|
||||
if(err.code === 'BINARY_FILE'){
|
||||
Files.url(this.state.path).then((url) => {
|
||||
this.setState({data: url, loading: false, opener: 'download'});
|
||||
}).catch(err => {
|
||||
notify.send(err, 'error');
|
||||
});
|
||||
this.setState({opener: 'download', loading: false});
|
||||
}else{
|
||||
notify.send(err, 'error');
|
||||
}
|
||||
});
|
||||
}else{
|
||||
Files.url(this.state.path).then((url) => {
|
||||
this.setState({data: url, loading: false, opener: app});
|
||||
}).catch(err => {
|
||||
if(err && err.code === 'CANCELLED'){ return; }
|
||||
notify.send(err, 'error');
|
||||
});
|
||||
this.setState({loading: false});
|
||||
}
|
||||
};
|
||||
return metadata()
|
||||
.then(data_fetch);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
@ -79,15 +83,16 @@ export class ViewerPage extends React.Component {
|
|||
|
||||
save(file){
|
||||
this.setState({isSaving: true});
|
||||
Files.save(this.state.path, file)
|
||||
return Files.save(this.state.path, file)
|
||||
.then(() => {
|
||||
this.setState({isSaving: false});
|
||||
this.setState({needSaving: false});
|
||||
this.setState({isSaving: false, needSaving: false});
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
if(err && err.code === 'CANCELLED'){ return; }
|
||||
this.setState({isSaving: false});
|
||||
notify.send(err, 'error');
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -116,23 +121,24 @@ export class ViewerPage extends React.Component {
|
|||
<IDE needSaving={this.needSaving.bind(this)}
|
||||
isSaving={this.state.isSaving}
|
||||
onSave={this.save.bind(this)}
|
||||
content={this.state.data || ''}
|
||||
content={this.state.content}
|
||||
url={this.state.url}
|
||||
filename={this.state.filename}/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.opener === 'image'} style={style}>
|
||||
<ImageViewer data={this.state.data} filename={this.state.filename} />
|
||||
<ImageViewer data={this.state.url} filename={this.state.filename} />
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.opener === 'pdf'} style={style}>
|
||||
<PDFViewer data={this.state.data} filename={this.state.filename} />
|
||||
<PDFViewer data={this.state.url} filename={this.state.filename} />
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.opener === 'video'} style={style}>
|
||||
<VideoPlayer data={this.state.data} filename={this.state.filename} />
|
||||
<VideoPlayer data={this.state.url} filename={this.state.filename} />
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.opener === 'audio'} style={style}>
|
||||
<AudioPlayer data={this.state.data} filename={this.state.filename} />
|
||||
<AudioPlayer data={this.state.url} filename={this.state.filename} />
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.opener === 'download'} style={style}>
|
||||
<FileDownloader data={this.state.data} filename={this.state.filename} />
|
||||
<FileDownloader data={this.state.url} filename={this.state.filename} />
|
||||
</NgIf>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.loading === true}>
|
||||
|
|
|
|||
|
|
@ -74,13 +74,6 @@ export class Editor extends React.Component {
|
|||
});
|
||||
|
||||
CodeMirror.commands.save = () => {
|
||||
let elt = editor.getWrapperElement();
|
||||
elt.style.background = "rgba(0,0,0,0.1)";
|
||||
elt.style.transition = "";
|
||||
window.setTimeout(function() {
|
||||
elt.style.transition = "background 0.5s ease-out";
|
||||
elt.style.background = "";
|
||||
}, 200);
|
||||
this.props.onSave && this.props.onSave();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
|||
|
||||
import { NgIf, Fab, Icon } from '../../components/';
|
||||
import { Editor } from './editor';
|
||||
import { MenuBar } from './menubar';
|
||||
|
||||
import './ide.scss';
|
||||
|
||||
|
|
@ -10,13 +11,14 @@ export class IDE extends React.Component {
|
|||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
contentToSave: props.content
|
||||
contentToSave: props.content,
|
||||
needSaving: false
|
||||
};
|
||||
}
|
||||
|
||||
onContentUpdate(text){
|
||||
this.props.needSaving(true);
|
||||
this.setState({contentToSave: text});
|
||||
this.setState({contentToSave: text, needSaving: true});
|
||||
}
|
||||
|
||||
save(){
|
||||
|
|
@ -28,21 +30,25 @@ export class IDE extends React.Component {
|
|||
// https://stackoverflow.com/questions/33821631/alternative-for-file-constructor-for-safari
|
||||
file = blob;
|
||||
}
|
||||
this.props.onSave(file);
|
||||
this.props.onSave(file)
|
||||
.then(() => this.setState({needSaving: false}));
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div style={{height: '100%'}}>
|
||||
<MenuBar title={this.props.filename} download={this.props.url} />
|
||||
<Editor onSave={this.save.bind(this)} filename={this.props.filename} content={this.props.content} onChange={this.onContentUpdate.bind(this)} />
|
||||
|
||||
<ReactCSSTransitionGroup transitionName="fab" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={25000}>
|
||||
<ReactCSSTransitionGroup transitionName="fab" transitionLeave={true} transitionEnter={true} transitionAppear={true} transitionAppearTimeout="300" transitonEnterTimeout="300" transitionLeaveTimeout="200">
|
||||
<NgIf key={this.state.needSaving} cond={this.state.needSaving}>
|
||||
<NgIf cond={!this.props.isSaving}>
|
||||
<Fab onClick={this.save.bind(this)}><Icon name="save" style={{height: '100%', width: '100%'}}/></Fab>
|
||||
</NgIf>
|
||||
<NgIf cond={this.props.isSaving}>
|
||||
<Fab><Icon name="loading_white" style={{height: '100%', width: '100%'}}/></Fab>
|
||||
</NgIf>
|
||||
</NgIf>
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
.fab-appear{
|
||||
opacity: 0;
|
||||
.fab-appear, .fab-enter{
|
||||
opacity: 0.5;
|
||||
transform: translateX(70px);
|
||||
}
|
||||
.fab-appear.fab-appear-active{
|
||||
.fab-appear.fab-appear-active, .fab-enter.fab-enter-active{
|
||||
transition: all 0.2s ease-out;
|
||||
transition-delay: 0.1s;
|
||||
transform: translateX(0px);
|
||||
opacity: 1;
|
||||
}
|
||||
.fab-leave{
|
||||
opacity: 1;
|
||||
}
|
||||
.fab-leave.fab-leave-active{
|
||||
transition: opacity 0.2s ease-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,71 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
|
||||
import { Container, NgIf, Icon } from '../../components/';
|
||||
import './menubar.scss';
|
||||
|
||||
export class MenuBar extends React.Component{
|
||||
|
||||
export const MenuBar = (props) => {
|
||||
return (
|
||||
<div className="component_menubar">
|
||||
<Container>
|
||||
<ReactCSSTransitionGroup transitionName="menubar" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={150}>
|
||||
<DownloadButton link={props.download} name={props.title} />
|
||||
<span style={{letterSpacing: '0.3px'}}>{props.title}</span>
|
||||
</ReactCSSTransitionGroup>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
class DownloadButton extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {loading: false, id: null}
|
||||
this.state = {
|
||||
loading: false,
|
||||
id: null
|
||||
};
|
||||
}
|
||||
|
||||
onDownloadRequest(){
|
||||
this.setState({
|
||||
loading: true,
|
||||
id: window.setInterval(function(){
|
||||
if(document.cookie){
|
||||
this.setState({loading: false})
|
||||
loading: true
|
||||
});
|
||||
|
||||
// This my friend is a dirty hack aiming to detect when we the download effectively start
|
||||
// so that we can display a spinner instead of having a user clicking the download button
|
||||
// 10 times. It works by sniffing a cookie in our session that will get destroy when
|
||||
// the server actually send a response
|
||||
document.cookie = "download=yes; path=/; max-age=120;";
|
||||
this.state.id = window.setInterval(() => {
|
||||
if(/download=yes/.test(document.cookie) === false){
|
||||
window.clearInterval(this.state.id);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
}.bind(this), 80)
|
||||
})
|
||||
}, 100);
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.clearInterval(this.state.id)
|
||||
window.clearInterval(this.state.id);
|
||||
}
|
||||
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div style={{background: '#313538', color: '#f1f1f1', boxShadow: 'rgba(0, 0, 0, 0.14) 2px 2px 2px 0px'}}>
|
||||
<Container style={{padding: '9px 0', textAlign: 'center', color: '#f1f1f1', fontSize: '0.9em'}}>
|
||||
<NgIf cond={this.props.hasOwnProperty('download')} style={{float: 'right', height: '1em'}}>
|
||||
<div style={{float: 'right', height: '1em'}}>
|
||||
<NgIf cond={!this.state.loading} style={{display: 'inline'}}>
|
||||
<a href={this.props.download} download={this.props.title} onClick={this.onDownloadRequest.bind(this)}>
|
||||
<a href={this.props.link} download={this.props.name} onClick={this.onDownloadRequest.bind(this)}>
|
||||
<Icon name="download" style={{width: '15px', height: '15px'}} />
|
||||
</a>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.loading} style={{display: 'inline'}}>
|
||||
<Icon name="loading" style={{width: '15px', height: '15px'}} />
|
||||
</NgIf>
|
||||
</NgIf>
|
||||
<span style={{letterSpacing: '0.3px'}}>{this.props.title}</span>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
DownloadButton.PropTypes = {
|
||||
link: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
};
|
||||
|
|
|
|||
24
client/pages/viewerpage/menubar.scss
Normal file
24
client/pages/viewerpage/menubar.scss
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
.component_menubar{
|
||||
background: #313538;
|
||||
color: #f1f1f1;
|
||||
border-bottom: 1px solid var(--color);
|
||||
|
||||
.component_container{
|
||||
padding: 8px 0;
|
||||
color: #f1f1f1;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menubar-appear{
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
.menubar-appear.menubar-appear-active{
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
transition: all 0.15s ease-out;
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ app.get('/ls', function(req, res){
|
|||
// get a file content
|
||||
app.get('/cat', function(req, res){
|
||||
let path = pathBuilder(req);
|
||||
res.cookie('download', path, { maxAge: 1000 });
|
||||
res.clearCookie("download");
|
||||
if(path){
|
||||
Files.cat(path, req.cookies.auth, res)
|
||||
.then(function(stream){
|
||||
|
|
@ -148,5 +148,5 @@ app.get('/touch', function(req, res){
|
|||
module.exports = app;
|
||||
|
||||
function pathBuilder(req){
|
||||
return path.join(req.cookies.auth.payload.path, decodeURIComponent(req.query.path));
|
||||
return path.join(req.cookies.auth.payload.path || '', decodeURIComponent(req.query.path) || '');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ app.post('/', function(req, res){
|
|||
if(Buffer.byteLength(cookie, 'utf-8') > 4096){
|
||||
res.send({status: 'error', message: 'we can\'t authenticate you', })
|
||||
}else{
|
||||
res.cookie('auth', crypto.encrypt(persist), { maxAge: 365*24*60*60*1000, httpOnly: true });
|
||||
res.cookie('auth', crypto.encrypt(persist), { maxAge: 365*24*60*60*1000, httpOnly: true, path: "/api/" });
|
||||
res.send({status: 'ok'});
|
||||
}
|
||||
})
|
||||
|
|
@ -43,7 +43,11 @@ app.post('/', function(req, res){
|
|||
});
|
||||
|
||||
app.delete('/', function(req, res){
|
||||
res.clearCookie("auth");
|
||||
res.clearCookie("auth", {path: "/api/"});
|
||||
|
||||
// TODO in May 2019: remove the line below which was inserted to mitigate a cookie migration issue.
|
||||
res.clearCookie("auth"); // the issue was a change in the cookie path which would have make
|
||||
// impossible for an existing user to logout
|
||||
res.send({status: 'ok'})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ var app = require('./bootstrap'),
|
|||
sessionRouter = require('./ctrl/session');
|
||||
|
||||
|
||||
app.get('/ping', function(req, res){ res.send('pong')})
|
||||
app.get('/api/ping', function(req, res){ res.send('pong')})
|
||||
app.use('/api/files', filesRouter)
|
||||
app.use('/api/session', sessionRouter);
|
||||
app.use('/', express.static(__dirname + '/public/'))
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ function smartCacheStrategy(request){
|
|||
.catch(function(err){
|
||||
return fetchAndCache(request);
|
||||
});
|
||||
});
|
||||
}).catch(() => return request);
|
||||
|
||||
|
||||
function fetchAndCache(_request){
|
||||
|
|
@ -110,7 +110,7 @@ function smartCacheStrategy(request){
|
|||
cache.put(_request, responseClone);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
}).catch(() => return _request);
|
||||
}
|
||||
function nil(e){}
|
||||
}
|
||||
|
|
@ -128,7 +128,7 @@ function networkFirstStrategy(request){
|
|||
network(request.clone && request.clone() || request)
|
||||
.then(done)
|
||||
.catch(error);
|
||||
});
|
||||
}).catch(() => return request);
|
||||
|
||||
function network(request){
|
||||
return fetch(request)
|
||||
|
|
|
|||
Loading…
Reference in a new issue