mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-28 04:05:21 +08:00
feature (orgmode): use emacs to export org documents
This commit is contained in:
@ -45,6 +45,9 @@ export class Dropdown extends React.Component {
|
||||
}
|
||||
|
||||
toggleDropdown(e){
|
||||
if(this.props.enable === false){
|
||||
return
|
||||
}
|
||||
document.body.removeEventListener("click", this.closeDropdown);
|
||||
this.setState({button: !this.state.button}, () => {
|
||||
if(this.state.button === true){
|
||||
|
||||
@ -4,7 +4,7 @@ export { debounce, throttle } from './backpressure';
|
||||
export { encrypt, decrypt, bcrypt_password } from './crypto';
|
||||
export { event } from './events';
|
||||
export { cache } from './cache';
|
||||
export { pathBuilder, basename, dirname, absoluteToRelative, filetype, currentShare, appendShareToUrl } from './path';
|
||||
export { pathBuilder, basename, dirname, absoluteToRelative, filetype, currentShare, findParams, appendShareToUrl } from './path';
|
||||
export { memory } from './memory';
|
||||
export { prepare } from './navigate';
|
||||
export { invalidate, http_get, http_post, http_delete } from './ajax';
|
||||
|
||||
@ -37,7 +37,11 @@ export function absoluteToRelative(from, to){
|
||||
}
|
||||
|
||||
export function currentShare(){
|
||||
return new window.URL(location.href).searchParams.get("share") || ""
|
||||
return findParams("share");
|
||||
}
|
||||
|
||||
export function findParams(p){
|
||||
return new window.URL(location.href).searchParams.get(p) || ""
|
||||
}
|
||||
|
||||
export function appendShareToUrl(link) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Card, NgIf, Icon, EventEmitter, EventReceiver, Dropdown, DropdownButton, DropdownList, DropdownItem } from '../../components/';
|
||||
import { Card, NgIf, Icon, EventEmitter, EventReceiver } from '../../components/';
|
||||
import { pathBuilder, debounce } from '../../helpers/';
|
||||
import "./thing.scss";
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Redirect } from 'react-router';
|
||||
|
||||
import { Share } from '../model/';
|
||||
import { notify, basename, filetype } from '../helpers/';
|
||||
import { notify, basename, filetype, findParams } from '../helpers/';
|
||||
import { Loader, Input, Button, Container, ErrorPage, Icon, NgIf } from '../components/';
|
||||
import './error.scss';
|
||||
import './sharepage.scss';
|
||||
@ -67,7 +67,20 @@ export class SharePage extends React.Component {
|
||||
let className = this.state.error ? "error rand-"+Math.random().toString() : "";
|
||||
|
||||
if(this.state.path !== null){
|
||||
if(filetype(this.state.path) === "directory"){
|
||||
if(!!findParams("next")){
|
||||
const url = findParams("next");
|
||||
if(url[0] === "/"){
|
||||
requestAnimationFrame(() => {
|
||||
window.location.pathname = url;
|
||||
});
|
||||
return (
|
||||
<div style={marginTop()}>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
notify.send("You can't do that :)", "error");
|
||||
}else if(filetype(this.state.path) === "directory"){
|
||||
return ( <Redirect to={`/files/?share=${this.state.share}`} /> );
|
||||
}else{
|
||||
return ( <Redirect to={`/view/${basename(this.state.path)}?nav=false&share=${this.state.share}`} /> );
|
||||
|
||||
@ -133,6 +133,7 @@ export class ViewerPage extends React.Component {
|
||||
onSave={this.save.bind(this)}
|
||||
content={this.state.content || ""}
|
||||
url={this.state.url}
|
||||
path={this.state.path}
|
||||
filename={this.state.filename}/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.opener === 'image'}>
|
||||
|
||||
@ -4,8 +4,8 @@ import { withRouter } from 'react-router';
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
import { NgIf, Fab, Icon } from '../../components/';
|
||||
import { confirm } from '../../helpers/';
|
||||
import { NgIf, Fab, Icon, Dropdown, DropdownButton, DropdownList, DropdownItem } from '../../components/';
|
||||
import { confirm, currentShare } from '../../helpers/';
|
||||
import { Editor } from './editor';
|
||||
import { MenuBar } from './menubar';
|
||||
import { OrgTodosViewer, OrgEventsViewer } from './org_viewer';
|
||||
@ -48,6 +48,7 @@ export class IDE extends React.Component {
|
||||
}
|
||||
componentWillUnmount(){
|
||||
this.unblock();
|
||||
window.clearInterval(this.state.id);
|
||||
}
|
||||
|
||||
save(){
|
||||
@ -70,7 +71,6 @@ export class IDE extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* Org Viewer specific stuff */
|
||||
toggleAgenda(force = null){
|
||||
this.setState({appear_agenda: force === null ? !this.state.appear_agenda : !!force});
|
||||
@ -85,10 +85,25 @@ export class IDE extends React.Component {
|
||||
this.state.event.next(["goTo", lineNumber]);
|
||||
}
|
||||
|
||||
download(){
|
||||
document.cookie = "download=yes; path=/; max-age=120;";
|
||||
this.setState({random: Math.random()});
|
||||
this.state.id = window.setInterval(() => {
|
||||
if(/download=yes/.test(document.cookie) === false){
|
||||
window.clearInterval(this.state.id);
|
||||
this.setState({random: Math.random()});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render(){
|
||||
const changeExt = function(filename, ext){
|
||||
return filename.replace(/\.org$/, "."+ext);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="component_ide">
|
||||
<MenuBar title={this.props.filename} download={this.props.url}>
|
||||
<MenuBar title={this.props.filename} download={this.state.mode === 'orgmode' ? null : this.props.url}>
|
||||
<NgIf type="inline" cond={this.state.mode === 'orgmode'}>
|
||||
<span onClick={this.onModeChange.bind(this)}>
|
||||
<NgIf cond={this.state.folding === "SHOW_ALL"} type="inline">
|
||||
@ -101,6 +116,22 @@ export class IDE extends React.Component {
|
||||
<Icon name="arrow_down_double"/>
|
||||
</NgIf>
|
||||
</span>
|
||||
<Dropdown className="view sort" onChange={() => this.download()} enable={/download=yes/.test(document.cookie) ? false : true}>
|
||||
<DropdownButton>
|
||||
<Icon name={/download=yes/.test(document.cookie) ? "loading_white" : "download_white"}/>
|
||||
</DropdownButton>
|
||||
<DropdownList>
|
||||
<DropdownItem name="na"><a download={this.props.filename} href={this.props.url}>Save current file</a></DropdownItem>
|
||||
<DropdownItem name="na"><a target={this.props.needSaving ? "_blank" : "_self"} href={"/api/export/"+(currentShare() || "me")+"/text/html"+this.props.path}>Export as HTML</a></DropdownItem>
|
||||
<DropdownItem name="na"><a target={this.props.needSaving ? "_blank" : "_self"} href={"/api/export/"+(currentShare() || "me")+"/application/pdf"+this.props.path}>Export as PDF</a></DropdownItem>
|
||||
<DropdownItem name="na"><a target={this.props.needSaving ? "_blank" : "_self"} href={"/api/export/"+(currentShare() || "me")+"/text/plain"+this.props.path}>Export as Text</a></DropdownItem>
|
||||
<DropdownItem name="na"><a target={this.props.needSaving ? "_blank" : "_self"} download={changeExt(this.props.filename, "tex")} href={"/api/export/"+(currentShare() || "me")+"/text/x-latex"+this.props.path}>Export as Latex</a></DropdownItem>
|
||||
<DropdownItem name="na"><a target={this.props.needSaving ? "_blank" : "_self"} download={changeExt(this.props.filename, "ics")} href={"/api/export/"+(currentShare() || "me")+"/text/calendar"+this.props.path}>Export as Calendar</a></DropdownItem>
|
||||
<DropdownItem name="na"><a target={this.props.needSaving ? "_blank" : "_self"} download={changeExt(this.props.filename, "pdf")} href={"/api/export/"+(currentShare() || "me")+"/application/pdf"+this.props.path+"?mode=beamer"}>Export as Beamer</a></DropdownItem>
|
||||
<DropdownItem name="na"><a target={this.props.needSaving ? "_blank" : "_self"} download={changeExt(this.props.filename, "odt")} href={"/api/export/"+(currentShare() || "me")+"/application/vnd.oasis.opendocument.text"+this.props.path}>Export as Open office</a></DropdownItem>
|
||||
<DropdownItem name="na"><a target={this.props.needSaving ? "_blank" : "_self"} href={"/api/export/"+(currentShare() || "me")+"/text/markdown"+this.props.path}>Export as Markdown</a></DropdownItem>
|
||||
</DropdownList>
|
||||
</Dropdown>
|
||||
|
||||
<span onClick={this.toggleAgenda.bind(this)}>
|
||||
<Icon name="calendar_white"/>
|
||||
|
||||
@ -9,6 +9,28 @@
|
||||
// https://stackoverflow.com/questions/44948158/flexbox-overflow-issue-in-firefox
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.component_menubar{
|
||||
.component_dropdown{
|
||||
float: right;
|
||||
.dropdown_button{
|
||||
border: none;
|
||||
padding: 0; margin: 0;
|
||||
}
|
||||
&.active .dropdown_button{
|
||||
box-shadow: none;
|
||||
}
|
||||
.dropdown_container ul li > div{
|
||||
padding: 0;
|
||||
a {
|
||||
padding: 7px 5px 7px 10px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ export const MenuBar = (props) => {
|
||||
<span className="specific">
|
||||
{props.children}
|
||||
</span>
|
||||
<DownloadButton link={props.download} name={props.title} />
|
||||
{ props.download === null ? null : <DownloadButton link={props.download} name={props.title} /> }
|
||||
</div>
|
||||
</ReactCSSTransitionGroup>
|
||||
</Container>
|
||||
|
||||
12
config/emacs.el
Normal file
12
config/emacs.el
Normal file
@ -0,0 +1,12 @@
|
||||
;; this is the config that's loaded by emacs when using the org mode export
|
||||
|
||||
;; org mode keywords
|
||||
(setq org-todo-keywords (quote ((sequence "TODO(t)" "DOING(d)" "WAITING(w)" "|" "CANCEL(C)" "DEFERRED(F)" "DONE(D)"))))
|
||||
|
||||
;; html export
|
||||
(setq org-html-head "<meta http-equiv='X-UA-Compatible' content='IE=edge'><meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' name='viewport'><style>html{touch-action:manipulation;-webkit-text-size-adjust:100%}body{padding:0;margin:0;background:#f2f6fa;color:#3c495a;font-weight:normal;font-size:16px;font-family:'avenir next','avenir','San Francisco','Roboto','Arial',sans-serif}h2,h3,h4,h5,h6{font-family:'Trebuchet MS',Verdana,sans-serif;color:#586b82;padding:0;margin:20px 0 10px 0;font-size:1.2em}h2{margin:30px 0 20px 0;font-size:1.5em}h2:after{display:block;content:' ';width:60px;border-bottom:3px solid #586b82;margin-top:5px;}li{text-align:left;}a{color:#3fa7ba;text-decoration:none}p{margin:10px 0;text-align:justify}ul,ol{margin:0;text-align:justify}#content ul,#content ol{margin-top:-5px;}#content ul>li>ul, #content ol>li>ol{margin-top:0;} ul>li>code{color:#586b82}pre{white-space:pre-wrap}pre.src{padding:10px}#content{width:96%;max-width:950px;margin:2% auto 5% auto;background:white;border-radius:2px;border-right:1px solid #e2e9f0;border-bottom:2px solid #e2e9f0;padding:0 115px 150px 115px;box-sizing:border-box}.org-src-container{margin-top:50px}#postamble{opacity:0.5;padding-bottom:10px;}#postamble .author{display:none}#postamble p{text-align:center;}h1.title{background-color:#343C44;color:#fff;margin:0 -115px 50px -115px;padding:60px 0;font-weight:normal;font-size:2em;border-top-left-radius:2px;border-top-right-radius:2px}@media (max-width: 1050px){#content{padding:0 70px 100px 70px}h1.title{margin:0 -70px 50px -70px}}@media (max-width: 800px){#content{width:100%;margin-top:0;padding:0 4% 60px 4%}h1.title{margin:0 -5% 50px -5%;padding:40px 5%}}pre,.verse{box-shadow:none;background-color:#f9fbfd;border:1px solid #e2e9f0;color:#586b82;padding:10px;font-family:monospace;overflow:auto;margin:6px 0}#table-of-contents{margin-bottom:50px;margin-top:50px}#table-of-contents h2{margin-bottom:15px}#text-table-of-contents ul{padding-left:15px}#text-table-of-contents>ul{padding-left:0}#text-table-of-contents li{list-style-type:none}#text-table-of-contents a{color:#7c8ca1;font-size:0.95em;text-decoration:none}table{border-color:#586b82;font-size:0.95em}table thead{color:#586b82}table tbody tr:nth-child(even){background:#f9f9f9}table tbody tr:hover{background:#586b82!important;color:white}table .left{text-align:left}table .right{text-align:right}.todo{font-family:inherit;color:inherit}.done{color:inherit}.tag{background:initial}.tag>span{background-color:#eee;font-family:monospace;padding-left:7px;padding-right:7px;border-radius:2px;float:right;margin-left:5px;font-size:14px;padding-bottom:2px;}#text-table-of-contents .tag>span{float:none;margin-left:0}.timestamp{color:#7c8ca1}@media print{@page{margin-bottom:3cm;margin-top:3cm;margin-left:2cm;margin-right:2cm;font-size:10px}#content{border:none}}</style>")
|
||||
(setq org-html-validation-link nil)
|
||||
(setq org-html-creator-string "Using <a href=\"http://filestash.app\">Filestash</a>")
|
||||
|
||||
(setq org-export-use-babel nil)
|
||||
(setq org-confirm-babel-evaluate nil)
|
||||
@ -90,6 +90,7 @@
|
||||
"ogg": "audio/ogg",
|
||||
"ogv": "application/ogg",
|
||||
"orf": "image/x-olympus-orf",
|
||||
"org": "text/org",
|
||||
"pdb": "application/x-pilot",
|
||||
"pdf": "application/pdf",
|
||||
"pef": "image/x-pentax-pef",
|
||||
|
||||
@ -5,6 +5,7 @@ const (
|
||||
CONFIG_PATH = "data/config/"
|
||||
PLUGIN_PATH = "data/plugin/"
|
||||
LOG_PATH = "data/log/"
|
||||
TMP_PATH = "data/tmp/"
|
||||
COOKIE_NAME_AUTH = "auth"
|
||||
COOKIE_NAME_PROOF = "proof"
|
||||
COOKIE_NAME_ADMIN = "admin"
|
||||
|
||||
@ -15,6 +15,9 @@ var (
|
||||
ErrNotValid error = NewError("Not Valid", 405)
|
||||
ErrNotReachable error = NewError("Cannot Reach Destination", 502)
|
||||
ErrInvalidPassword = NewError("Invalid Password", 403)
|
||||
ErrNotImplemented = NewError("Not Implemented", 501)
|
||||
ErrFilesystemError = NewError("Can't use filesystem", 503)
|
||||
ErrMissingDependency = NewError("Missing dependency", 424)
|
||||
)
|
||||
|
||||
type AppError struct {
|
||||
|
||||
149
server/ctrl/export.go
Normal file
149
server/ctrl/export.go
Normal file
@ -0,0 +1,149 @@
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
"github.com/mickael-kerjean/filestash/server/model"
|
||||
"github.com/gorilla/mux"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
var EXPORT_PATH string
|
||||
func init() {
|
||||
EXPORT_PATH = GetAbsolutePath(TMP_PATH)
|
||||
os.RemoveAll(EXPORT_PATH)
|
||||
os.MkdirAll(EXPORT_PATH, os.ModePerm)
|
||||
}
|
||||
func FileExport(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
query := req.URL.Query()
|
||||
p := mux.Vars(req)
|
||||
mimeType := fmt.Sprintf("%s/%s", p["mtype0"], p["mtype1"])
|
||||
path, err := pathBuilder(ctx, strings.Replace(req.URL.Path, fmt.Sprintf("/api/export/%s/%s/%s", p["share"], p["mtype0"], p["mtype1"]), "", 1))
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
} else if model.CanRead(&ctx) == false {
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
http.SetCookie(res, &http.Cookie{
|
||||
Name: "download",
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
var tmpPath string = EXPORT_PATH + "/export_" + QuickString(10)
|
||||
var cmd *exec.Cmd
|
||||
var emacsPath string
|
||||
var outPath string
|
||||
if GetMimeType(path) == "text/org" {
|
||||
if emacsPath, err = exec.LookPath("emacs"); err != nil {
|
||||
SendErrorResult(res, ErrMissingDependency)
|
||||
return
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
// on OSX, the default emacs isn't usable so we default to the one provided by `brew`
|
||||
if f, err := os.OpenFile("/usr/local/Cellar/emacs/", os.O_RDONLY, os.ModePerm); err == nil {
|
||||
if dirs, err := f.Readdirnames(0); err == nil {
|
||||
if len(dirs) > 0 {
|
||||
emacsPath = "/usr/local/Cellar/emacs/" + dirs[0] + "/bin/emacs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mimeType == "text/html" {
|
||||
cmd = exec.Command(
|
||||
emacsPath, "--no-init-file", "--batch",
|
||||
"--load", GetAbsolutePath(CONFIG_PATH + "emacs.el"),
|
||||
"--eval", "(setq org-html-extension \"org\")",
|
||||
tmpPath + "/index.org", "-f", "org-html-export-to-html",
|
||||
)
|
||||
outPath = "index.org.org"
|
||||
} else if mimeType == "application/pdf" {
|
||||
cmd = exec.Command(
|
||||
emacsPath, "--no-init-file", "--batch",
|
||||
tmpPath + "/index.org", "-f", "org-latex-export-to-pdf",
|
||||
)
|
||||
if query.Get("mode") == "beamer" {
|
||||
cmd = exec.Command(
|
||||
emacsPath, "--no-init-file", "--batch",
|
||||
tmpPath + "/index.org", "-f", "org-beamer-export-to-pdf",
|
||||
)
|
||||
}
|
||||
outPath = "index.pdf"
|
||||
} else if mimeType == "text/calendar" {
|
||||
cmd = exec.Command(
|
||||
emacsPath, "--no-init-file", "--batch",
|
||||
tmpPath + "/index.org", "-f", "org-icalendar-export-to-ics",
|
||||
)
|
||||
outPath = "index.ics"
|
||||
} else if mimeType == "text/plain" {
|
||||
cmd = exec.Command(
|
||||
emacsPath, "--no-init-file", "--batch",
|
||||
tmpPath + "/index.org", "-f", "org-ascii-export-to-ascii",
|
||||
)
|
||||
outPath = "index.txt"
|
||||
} else if mimeType == "text/x-latex" {
|
||||
cmd = exec.Command(
|
||||
emacsPath, "--no-init-file", "--batch",
|
||||
tmpPath + "/index.org", "-f", "org-latex-export-to-latex",
|
||||
)
|
||||
outPath = "index.tex"
|
||||
} else if mimeType == "text/markdown" {
|
||||
cmd = exec.Command(
|
||||
emacsPath, "--no-init-file", "--batch",
|
||||
tmpPath + "/index.org", "-f", "org-md-export-to-markdown",
|
||||
)
|
||||
outPath = "index.md"
|
||||
} else if mimeType == "application/vnd.oasis.opendocument.text" {
|
||||
cmd = exec.Command(
|
||||
emacsPath, "--no-init-file", "--batch",
|
||||
tmpPath + "/index.org", "-f", "org-odt-export-to-odt",
|
||||
)
|
||||
outPath = "index.odt"
|
||||
}else {
|
||||
SendErrorResult(res, ErrNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
os.MkdirAll(tmpPath, os.ModePerm)
|
||||
defer os.RemoveAll(tmpPath)
|
||||
f, err := os.OpenFile(tmpPath + "/index.org", os.O_WRONLY|os.O_CREATE, os.ModePerm)
|
||||
if err != nil {
|
||||
SendErrorResult(res, ErrFilesystemError)
|
||||
return
|
||||
}
|
||||
file, err := ctx.Backend.Cat(path)
|
||||
if err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
io.Copy(f, file)
|
||||
// TODO: insert related resources: eg: images
|
||||
|
||||
if err = cmd.Run(); err != nil {
|
||||
SendErrorResult(res, NewError("emacs has quit with error: '%s'" + err.Error(), 400))
|
||||
return
|
||||
}
|
||||
|
||||
f, err = os.OpenFile(tmpPath + "/"+outPath, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
SendErrorResult(res, ErrFilesystemError)
|
||||
return
|
||||
}
|
||||
res.Header().Set("Content-Type", mimeType)
|
||||
io.Copy(res, f)
|
||||
return
|
||||
}
|
||||
|
||||
SendErrorResult(res, ErrNotImplemented)
|
||||
return
|
||||
}
|
||||
@ -106,7 +106,7 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
Path: "/",
|
||||
})
|
||||
if model.CanRead(&ctx) == false {
|
||||
SendErrorResult(res, NewError("Permission denied", 403))
|
||||
SendErrorResult(res, ErrPermissionDenied)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -151,7 +151,7 @@ func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
Key: fmt.Sprint(ctx.Body["type"]),
|
||||
Value: fmt.Sprint(ctx.Body["value"]),
|
||||
}
|
||||
verifiedProof = model.ShareProofGetAlreadyVerified(req, &ctx)
|
||||
verifiedProof = model.ShareProofGetAlreadyVerified(req)
|
||||
requiredProof = model.ShareProofGetRequired(s)
|
||||
|
||||
// 2) validate the current context
|
||||
|
||||
@ -66,21 +66,27 @@ func Init(a *App) {
|
||||
// API for File management
|
||||
files := r.PathPrefix("/api/files").Subrouter()
|
||||
middlewares = []Middleware{ ApiHeaders, SecureHeaders, SessionStart, LoggedInOnly }
|
||||
files.HandleFunc("/ls", NewMiddlewareChain(FileLs, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", NewMiddlewareChain(FileCat, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", NewMiddlewareChain(FileSave, middlewares, *a)).Methods("POST")
|
||||
files.HandleFunc("/mv", NewMiddlewareChain(FileMv, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/rm", NewMiddlewareChain(FileRm, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/mkdir", NewMiddlewareChain(FileMkdir, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/touch", NewMiddlewareChain(FileTouch, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/ls", NewMiddlewareChain(FileLs, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", NewMiddlewareChain(FileCat, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/cat", NewMiddlewareChain(FileSave, middlewares, *a)).Methods("POST")
|
||||
files.HandleFunc("/mv", NewMiddlewareChain(FileMv, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/rm", NewMiddlewareChain(FileRm, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/mkdir", NewMiddlewareChain(FileMkdir, middlewares, *a)).Methods("GET")
|
||||
files.HandleFunc("/touch", NewMiddlewareChain(FileTouch, middlewares, *a)).Methods("GET")
|
||||
|
||||
// API for exporter
|
||||
middlewares = []Middleware{ ApiHeaders, SecureHeaders, RedirectSharedLoginIfNeeded, SessionStart, LoggedInOnly }
|
||||
r.PathPrefix("/api/export/{share}/{mtype0}/{mtype1}").Handler(NewMiddlewareChain(FileExport, middlewares, *a))
|
||||
|
||||
|
||||
// API for Shared link
|
||||
share := r.PathPrefix("/api/share").Subrouter()
|
||||
middlewares = []Middleware{ ApiHeaders, SecureHeaders, SessionStart, LoggedInOnly }
|
||||
share.HandleFunc("", NewMiddlewareChain(ShareList, middlewares, *a)).Methods("GET")
|
||||
share.HandleFunc("/{share}", NewMiddlewareChain(ShareDelete, middlewares, *a)).Methods("DELETE")
|
||||
middlewares = []Middleware{ ApiHeaders, SecureHeaders, SessionStart, BodyParser, LoggedInOnly }
|
||||
middlewares = []Middleware{ ApiHeaders, SecureHeaders, SessionStart, LoggedInOnly, BodyParser }
|
||||
share.HandleFunc("/{share}", NewMiddlewareChain(ShareUpsert, middlewares, *a)).Methods("POST")
|
||||
middlewares = []Middleware{ ApiHeaders, SecureHeaders, BodyParser }
|
||||
share.HandleFunc("/{share}/proof", NewMiddlewareChain(ShareVerifyProof, middlewares, *a)).Methods("POST")
|
||||
|
||||
// Webdav server / Shared Link
|
||||
|
||||
@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
"github.com/mickael-kerjean/filestash/server/model"
|
||||
"github.com/gorilla/mux"
|
||||
@ -46,45 +47,18 @@ func AdminOnly(fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, r
|
||||
}
|
||||
|
||||
func SessionStart (fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
extractShare := func(req *http.Request, ctx *App, share_id string) (Share, error) {
|
||||
if share_id == "" {
|
||||
return Share{}, nil
|
||||
}
|
||||
|
||||
if Config.Get("features.share.enable").Bool() == false {
|
||||
Log.Debug("Share feature isn't enable, contact your administrator")
|
||||
return Share{}, NewError("Feature isn't enable, contact your administrator", 405)
|
||||
}
|
||||
|
||||
s, err := model.ShareGet(share_id)
|
||||
if err != nil {
|
||||
return Share{}, nil
|
||||
}
|
||||
if err = s.IsValid(); err != nil {
|
||||
return Share{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
extractSession := func(req *http.Request, ctx *App) (map[string]string, error) {
|
||||
var str string
|
||||
var err error
|
||||
var res map[string]string = make(map[string]string)
|
||||
|
||||
if ctx.Share.Id != "" {
|
||||
var verifiedProof []model.Proof = model.ShareProofGetAlreadyVerified(req, ctx)
|
||||
var requiredProof []model.Proof = model.ShareProofGetRequired(ctx.Share)
|
||||
var remainingProof []model.Proof = model.ShareProofCalculateRemainings(requiredProof, verifiedProof)
|
||||
if len(remainingProof) != 0 {
|
||||
return res, NewError("Unauthorized Shared space", 400)
|
||||
}
|
||||
str = ctx.Share.Auth
|
||||
str, err = DecryptString(SECRET_KEY, str)
|
||||
str, err = DecryptString(SECRET_KEY, ctx.Share.Auth)
|
||||
if err != nil {
|
||||
// This typically happen when changing the secret key
|
||||
return res, nil
|
||||
}
|
||||
err = json.Unmarshal([]byte(str), &res)
|
||||
|
||||
if ctx.Share.Path[len(ctx.Share.Path)-1:] == "/" {
|
||||
res["path"] = ctx.Share.Path
|
||||
} else {
|
||||
@ -116,14 +90,7 @@ func SessionStart (fn func(App, http.ResponseWriter, *http.Request)) func(ctx Ap
|
||||
|
||||
return func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
var err error
|
||||
share_id := func() string {
|
||||
if len(req.URL.Path) > 3 && req.URL.Path[:3] == "/s/" {
|
||||
// this runs while using a link as a webdav server
|
||||
return mux.Vars(req)["share"]
|
||||
}
|
||||
return req.URL.Query().Get("share")
|
||||
}()
|
||||
if ctx.Share, err = extractShare(req, &ctx, share_id); err != nil {
|
||||
if ctx.Share, err = _findShare(req, _extractShareId(req)); err != nil {
|
||||
SendErrorResult(res, err)
|
||||
return
|
||||
}
|
||||
@ -138,3 +105,60 @@ func SessionStart (fn func(App, http.ResponseWriter, *http.Request)) func(ctx Ap
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func RedirectSharedLoginIfNeeded(fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
return func(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||
share_id := _extractShareId(req)
|
||||
if share_id == "" {
|
||||
SendErrorResult(res, ErrNotValid)
|
||||
return
|
||||
}
|
||||
|
||||
share, err := _findShare(req, share_id);
|
||||
if err != nil || share_id != share.Id {
|
||||
http.Redirect(res, req, fmt.Sprintf("/s/%s?next=%s", share_id, req.URL.Path), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
fn(ctx, res, req)
|
||||
}
|
||||
}
|
||||
|
||||
func _extractShareId(req *http.Request) string {
|
||||
share := req.URL.Query().Get("share")
|
||||
if share != "" {
|
||||
return share
|
||||
}
|
||||
m := mux.Vars(req)["share"]
|
||||
if m == "me" {
|
||||
return ""
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func _findShare(req *http.Request, share_id string) (Share, error) {
|
||||
var err error
|
||||
if share_id == "" {
|
||||
return Share{}, nil
|
||||
}
|
||||
|
||||
if Config.Get("features.share.enable").Bool() == false {
|
||||
Log.Debug("Share feature isn't enable, contact your administrator")
|
||||
return Share{}, NewError("Feature isn't enable, contact your administrator", 405)
|
||||
}
|
||||
|
||||
s, err := model.ShareGet(share_id)
|
||||
if err != nil {
|
||||
return Share{}, nil
|
||||
}
|
||||
if err = s.IsValid(); err != nil {
|
||||
return Share{}, err
|
||||
}
|
||||
|
||||
var verifiedProof []model.Proof = model.ShareProofGetAlreadyVerified(req)
|
||||
var requiredProof []model.Proof = model.ShareProofGetRequired(s)
|
||||
var remainingProof []model.Proof = model.ShareProofCalculateRemainings(requiredProof, verifiedProof)
|
||||
if len(remainingProof) != 0 {
|
||||
return Share{}, NewError("Unauthorized Shared space", 400)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@ -249,7 +249,7 @@ func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func ShareProofGetAlreadyVerified(req *http.Request, ctx *App) []Proof {
|
||||
func ShareProofGetAlreadyVerified(req *http.Request) []Proof {
|
||||
var p []Proof
|
||||
var cookieValue string
|
||||
|
||||
|
||||
@ -165,7 +165,7 @@ func Init(conf *Configuration) {
|
||||
// => lower RAM usage while processing
|
||||
file, err := os.OpenFile(transform.Temporary, os.O_WRONLY|os.O_CREATE, os.ModePerm)
|
||||
if err != nil {
|
||||
return reader, NewError("Can't use filesystem", 500)
|
||||
return reader, ErrFilesystemError
|
||||
}
|
||||
io.Copy(file, reader)
|
||||
file.Close()
|
||||
|
||||
Reference in New Issue
Block a user