mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-06 08:22:24 +01:00
migration: migrate NodeJS code base to Golang
This commit is contained in:
parent
c5f2839fd7
commit
04c97e34fb
68 changed files with 3837 additions and 2217 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
babel_cache/
|
|
||||||
dist/
|
dist/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
|
||||||
|
|
@ -40,19 +40,17 @@
|
||||||
# Getting started - Installation
|
# Getting started - Installation
|
||||||
Nuage can be used in different settings:
|
Nuage can be used in different settings:
|
||||||
- Selfhosting ([documentation](https://github.com/mickael-kerjean/nuage/wiki/Installation:-Selfhosting)): install it somewhere you have full control (with docker, without docker, on a server or even android)
|
- Selfhosting ([documentation](https://github.com/mickael-kerjean/nuage/wiki/Installation:-Selfhosting)): install it somewhere you have full control (with docker, without docker, on a server or even android)
|
||||||
- Paas ([documentation](https://github.com/mickael-kerjean/nuage/wiki/Installation:-PaaS)): deployment on Heroku or AWS Lambda
|
|
||||||
- Saas ([documentation](https://github.com/mickael-kerjean/nuage/wiki/Installation:-SaaS)): official instance or private instance fully managed
|
- Saas ([documentation](https://github.com/mickael-kerjean/nuage/wiki/Installation:-SaaS)): official instance or private instance fully managed
|
||||||
|
|
||||||
# Support the project
|
# Support the project
|
||||||
- Bitcoin: `3LX5KGmSmHDj5EuXrmUvcg77EJxCxmdsgW`
|
- Bitcoin: `3LX5KGmSmHDj5EuXrmUvcg77EJxCxmdsgW`
|
||||||
- [Patreon](https://www.patreon.com/mickaelk)
|
- [Patreon](https://www.patreon.com/mickaelk)
|
||||||
|
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
- [FAQ](https://github.com/mickael-kerjean/nuage/wiki)
|
- [FAQ](https://github.com/mickael-kerjean/nuage/wiki)
|
||||||
- [Customisation](https://github.com/mickael-kerjean/nuage/wiki/Customisation)
|
- [Customisation](https://github.com/mickael-kerjean/nuage/wiki/Customisation)
|
||||||
- [Release Notes](https://github.com/mickael-kerjean/nuage/wiki/Releases)
|
- [Release Notes](https://github.com/mickael-kerjean/nuage/wiki/Releases)
|
||||||
|
|
||||||
# Credits
|
# Credits
|
||||||
- [Contributors](https://github.com/mickael-kerjean/nuage/graphs/contributors) and folks developing awesome [libraries](https://github.com/mickael-kerjean/nuage/blob/master/package.json)
|
- [Contributors](https://github.com/mickael-kerjean/nuage/graphs/contributors) and folks developing awesome libraries (libvips, libraw, ...)
|
||||||
- Logo by [ssnjrthegr8](https://github.com/ssnjrthegr8) and Iconography from [flaticon](https://www.flaticon.com/), [fontawesome](https://fontawesome.com) and [material](https://material.io/icons/)
|
- Logo by [ssnjrthegr8](https://github.com/ssnjrthegr8) and Iconography from [flaticon](https://www.flaticon.com/), [fontawesome](https://fontawesome.com) and [material](https://material.io/icons/)
|
||||||
|
|
|
||||||
BIN
client/components/textarea.woff
Normal file
BIN
client/components/textarea.woff
Normal file
Binary file not shown.
|
|
@ -49,7 +49,7 @@ Data.prototype.get = function(type, path){
|
||||||
};
|
};
|
||||||
query.onerror = error;
|
query.onerror = error;
|
||||||
});
|
});
|
||||||
});
|
}).catch(() => Promise.resolve(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.prototype.update = function(type, path, fn, exact = true){
|
Data.prototype.update = function(type, path, fn, exact = true){
|
||||||
|
|
@ -73,7 +73,7 @@ Data.prototype.update = function(type, path, fn, exact = true){
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
}).catch(() => Promise.resolve(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ Data.prototype.upsert = function(type, path, fn){
|
||||||
};
|
};
|
||||||
query.onerror = error;
|
query.onerror = error;
|
||||||
});
|
});
|
||||||
});
|
}).catch(() => Promise.resolve(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.prototype.add = function(type, path, data){
|
Data.prototype.add = function(type, path, data){
|
||||||
|
|
@ -107,7 +107,7 @@ Data.prototype.add = function(type, path, data){
|
||||||
request.onsuccess = () => done(data);
|
request.onsuccess = () => done(data);
|
||||||
request.onerror = (e) => error(e);
|
request.onerror = (e) => error(e);
|
||||||
});
|
});
|
||||||
});
|
}).catch(() => Promise.resolve(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.prototype.remove = function(type, path, exact = true){
|
Data.prototype.remove = function(type, path, exact = true){
|
||||||
|
|
@ -139,7 +139,7 @@ Data.prototype.remove = function(type, path, exact = true){
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}).catch(() => Promise.resolve(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.prototype.fetchAll = function(fn, type = this.FILE_PATH){
|
Data.prototype.fetchAll = function(fn, type = this.FILE_PATH){
|
||||||
|
|
@ -156,11 +156,13 @@ Data.prototype.fetchAll = function(fn, type = this.FILE_PATH){
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
}).catch(() => Promise.resolve(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.prototype.destroy = function(){
|
Data.prototype.destroy = function(){
|
||||||
this.db.then((db) => db.close())
|
this.db
|
||||||
|
.then((db) => db.close())
|
||||||
|
.catch(() => {})
|
||||||
clearTimeout(this.intervalId);
|
clearTimeout(this.intervalId);
|
||||||
window.indexedDB.deleteDatabase('nuage');
|
window.indexedDB.deleteDatabase('nuage');
|
||||||
this._init();
|
this._init();
|
||||||
|
|
@ -168,4 +170,3 @@ Data.prototype.destroy = function(){
|
||||||
|
|
||||||
|
|
||||||
export const cache = new Data();
|
export const cache = new Data();
|
||||||
window._cache = cache;
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import Path from 'path';
|
import Path from 'path';
|
||||||
import db from '../../server/common/mimetype.json';
|
|
||||||
|
|
||||||
export function getMimeType(file){
|
export function getMimeType(file){
|
||||||
let ext = Path.extname(file).replace(/^\./, '').toLowerCase();
|
let ext = Path.extname(file).replace(/^\./, '').toLowerCase();
|
||||||
let mime = db[ext];
|
let mime = CONFIG.mime[ext];
|
||||||
if(mime){
|
if(mime){
|
||||||
return mime;
|
return mime;
|
||||||
}else{
|
}else{
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,8 @@
|
||||||
<link rel="icon" type="image/png" sizes="96x96" href="/assets/logo/favicon-96x96.png">
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/logo/favicon-96x96.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/logo/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/logo/favicon-16x16.png">
|
||||||
<link rel="icon" href="/assets/logo/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/assets/logo/favicon.ico" type="image/x-icon" />
|
||||||
<link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet">
|
|
||||||
<script src="/api/config"></script>
|
<script src="/api/config"></script>
|
||||||
|
|
||||||
<!--[if IE]>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.min.js"></script>
|
|
||||||
<![endif]-->
|
|
||||||
|
|
||||||
<meta name="msapplication-TileColor" content="#f2f2f2">
|
<meta name="msapplication-TileColor" content="#f2f2f2">
|
||||||
<meta name="msapplication-TileImage" content="/assets/logo/ms-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="/assets/logo/ms-icon-144x144.png">
|
||||||
<meta name="theme-color" content="#f2f2f2">
|
<meta name="theme-color" content="#f2f2f2">
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,9 @@ const S3Form = formHelper(function(props){
|
||||||
<NgIf cond={props.should_appear("path")}>
|
<NgIf cond={props.should_appear("path")}>
|
||||||
<Input value={props.values["path"] || ""} onChange={(e) => props.onChange("path", e.target.value)} type={props.input_type("path")} name="path" placeholder="Path" autoComplete="new-password" />
|
<Input value={props.values["path"] || ""} onChange={(e) => props.onChange("path", e.target.value)} type={props.input_type("path")} name="path" placeholder="Path" autoComplete="new-password" />
|
||||||
</NgIf>
|
</NgIf>
|
||||||
|
<NgIf cond={props.should_appear("region")}>
|
||||||
|
<Input value={props.values["region"] || ""} onChange={(e) => props.onChange("region", e.target.value)} type={props.input_type("region")} name="region" placeholder="Region" autoComplete="new-password" />
|
||||||
|
</NgIf>
|
||||||
<NgIf cond={props.should_appear("endpoint")}>
|
<NgIf cond={props.should_appear("endpoint")}>
|
||||||
<Input value={props.values["endpoint"] || ""} onChange={(e) => props.onChange("endpoint", e.target.value)} type={props.input_type("endpoint")} name="endpoint" placeholder="Endpoint" autoComplete="new-password" />
|
<Input value={props.values["endpoint"] || ""} onChange={(e) => props.onChange("endpoint", e.target.value)} type={props.input_type("endpoint")} name="endpoint" placeholder="Endpoint" autoComplete="new-password" />
|
||||||
</NgIf>
|
</NgIf>
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export class ExistingThing extends React.Component {
|
||||||
const type = getMimeType(_path).split("/")[0];
|
const type = getMimeType(_path).split("/")[0];
|
||||||
if(type === "image"){
|
if(type === "image"){
|
||||||
Files.url(_path).then((url) => {
|
Files.url(_path).then((url) => {
|
||||||
this.setState({preview: url+"&size=250"});
|
this.setState({preview: url+"&thumbnail=true"});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ class Img extends React.Component{
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
const image_url = (url, size) => {
|
const image_url = (url, size) => {
|
||||||
return url+"&meta=true&size="+parseInt(window.innerWidth*size);
|
return url+"&size="+parseInt(window.innerWidth*size);
|
||||||
};
|
};
|
||||||
if(!this.props.src) return null;
|
if(!this.props.src) return null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const CACHE_NAME = 'v1.0';
|
const CACHE_NAME = 'v0.3';
|
||||||
const DELAY_BEFORE_SENDING_CACHE = 2000;
|
const DELAY_BEFORE_SENDING_CACHE = 2000;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
56
config/config.json
Normal file
56
config/config.json
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"port": 8334,
|
||||||
|
"host": "http://127.0.0.1:8334",
|
||||||
|
"secret_key": "example key 1234",
|
||||||
|
"editor": "emacs",
|
||||||
|
"fork_button": true,
|
||||||
|
"display_hidden": false,
|
||||||
|
"client_search_enable": true,
|
||||||
|
"client_search_per_min": 20
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"enable": true,
|
||||||
|
"level": "INFO",
|
||||||
|
"telemetry": true
|
||||||
|
},
|
||||||
|
"oauth": {
|
||||||
|
"gdrive": {
|
||||||
|
"client_id": "",
|
||||||
|
"client_secret": ""
|
||||||
|
},
|
||||||
|
"dropbox": {
|
||||||
|
"client_id": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"type": "webdav",
|
||||||
|
"label": "WebDav"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ftp",
|
||||||
|
"label": "FTP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "sftp",
|
||||||
|
"label": "SFTP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "git",
|
||||||
|
"label": "GIT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "s3",
|
||||||
|
"label": "S3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dropbox",
|
||||||
|
"label": "Dropbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "gdrive",
|
||||||
|
"label": "Drive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,35 +1,4 @@
|
||||||
const path = require('path');
|
{
|
||||||
|
|
||||||
module.exports.getMimeType = function(file){
|
|
||||||
let ext = path.extname(file).replace(/^\./, '').toLowerCase();
|
|
||||||
let mime = db[ext];
|
|
||||||
if(mime){
|
|
||||||
return mime;
|
|
||||||
}else{
|
|
||||||
return 'text/plain';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.opener = function(file){
|
|
||||||
let mime = getMimeType(file);
|
|
||||||
if(mime.split('/')[0] === 'text'){
|
|
||||||
return 'editor';
|
|
||||||
}else if(mime === 'application/pdf'){
|
|
||||||
return 'pdf';
|
|
||||||
}else if(mime.split('/')[0] === 'image'){
|
|
||||||
return 'image';
|
|
||||||
}else if(['application/javascript', 'application/xml', 'application/x-perl'].indexOf(mime) !== -1){
|
|
||||||
return 'editor';
|
|
||||||
}else if(['audio/wav', 'audio/mp3', 'audio/flac'].indexOf(mime) !== -1){
|
|
||||||
return 'audio';
|
|
||||||
}else if(['video/webm', 'video/mp4', 'application/ogg'].indexOf(mime) !== -1){
|
|
||||||
return 'video';
|
|
||||||
}else{
|
|
||||||
return 'download';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = {
|
|
||||||
"html": "text/html",
|
"html": "text/html",
|
||||||
"shtml": "text/html",
|
"shtml": "text/html",
|
||||||
"htm": "text/html",
|
"htm": "text/html",
|
||||||
|
|
@ -45,7 +14,6 @@ const db = {
|
||||||
"jpeg": "image/jpeg",
|
"jpeg": "image/jpeg",
|
||||||
"svg": "image/svg",
|
"svg": "image/svg",
|
||||||
"png": "image/png",
|
"png": "image/png",
|
||||||
"svg": "image/svg+xml",
|
|
||||||
"svgz": "image/svg+xml",
|
"svgz": "image/svg+xml",
|
||||||
"webp": "image/webp",
|
"webp": "image/webp",
|
||||||
"gif": "image/gif",
|
"gif": "image/gif",
|
||||||
|
|
@ -228,9 +196,7 @@ const db = {
|
||||||
"msi": "application/octet-stream",
|
"msi": "application/octet-stream",
|
||||||
"msm": "application/octet-stream",
|
"msm": "application/octet-stream",
|
||||||
"msp": "application/octet-stream",
|
"msp": "application/octet-stream",
|
||||||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
"docx": "application/word",
|
||||||
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
"xlsx": "application/excel",
|
||||||
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
"pptx": "application/powerpoint"
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports.mime = db;
|
|
||||||
|
|
@ -1,21 +1,56 @@
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
MAINTAINER mickael.kerjean@gmail.com
|
MAINTAINER mickael.kerjean@gmail.com
|
||||||
|
|
||||||
RUN apk add --no-cache git && \
|
RUN mkdir -p /tmp/go/src/github.com/mickael-kerjean/ && \
|
||||||
# INSTALL SYSTEM DEPS
|
#################
|
||||||
git clone https://github.com/mickael-kerjean/nuage /app && \
|
# Dependencies
|
||||||
cd /app && \
|
apk --no-cache --virtual .build-deps add make gcc g++ curl nodejs git go && \
|
||||||
apk add --no-cache nodejs libcurl && \
|
mkdir /tmp/deps && \
|
||||||
# Nodegit
|
# libvips #######
|
||||||
apk --no-cache add --virtual .build-deps g++ libressl-dev make python curl-dev && \
|
cd /tmp/deps && \
|
||||||
BUILD_ONLY=true npm install nodegit > /dev/null && \
|
curl -L -X GET https://github.com/jcupitt/libvips/releases/download/v8.6.5/vips-8.6.5.tar.gz > libvips.tar.gz && \
|
||||||
apk del .build-deps && \
|
tar -zxf libvips.tar.gz && \
|
||||||
|
cd vips-8.6.5/ && \
|
||||||
|
apk --no-cache add libexif-dev tiff-dev jpeg-dev libjpeg-turbo-dev libpng-dev librsvg-dev giflib-dev glib-dev fftw-dev glib-dev libc-dev expat-dev orc-dev && \
|
||||||
|
./configure && \
|
||||||
|
make -j 6 && \
|
||||||
|
make install && \
|
||||||
|
# libraw ########
|
||||||
|
cd /tmp/deps && \
|
||||||
|
curl -X GET https://www.libraw.org/data/LibRaw-0.19.0.tar.gz > libraw.tar.gz && \
|
||||||
|
tar -zxf libraw.tar.gz && \
|
||||||
|
cd LibRaw-0.19.0/ && \
|
||||||
|
./configure && \
|
||||||
|
make -j 6 && \
|
||||||
|
make install && \
|
||||||
|
#################
|
||||||
|
# Prepare Build
|
||||||
|
cd /tmp/go/src/github.com/mickael-kerjean && \
|
||||||
|
apk add --no-cache --virtual .build-deps git go nodejs && \
|
||||||
|
git clone --depth 1 https://github.com/mickael-kerjean/nuage && \
|
||||||
|
cd nuage && \
|
||||||
|
mkdir -p ./dist/data/ && \
|
||||||
|
mv config ./dist/data/ && \
|
||||||
|
#################
|
||||||
|
# Compile Frontend
|
||||||
npm install && \
|
npm install && \
|
||||||
# PRODUCTION BUILD
|
npm rebuild node-sass && \
|
||||||
NODE_ENV=production npm run build && \
|
NODE_ENV=production npm run build && \
|
||||||
npm prune --production
|
#################
|
||||||
|
# Compile Backend
|
||||||
|
cd /tmp/go/src/github.com/mickael-kerjean/nuage/server && \
|
||||||
|
CGO_CFLAGS_ALLOW='-fopenmp' GOPATH=/tmp/go go get && \
|
||||||
|
cd ../ && \
|
||||||
|
GOPATH=/tmp/go go build -o ./dist/nuage ./server/main.go && \
|
||||||
|
#################
|
||||||
|
# Finalise the build
|
||||||
|
apk --no-cache add ca-certificates && \
|
||||||
|
mv dist /app && \
|
||||||
|
cd /app && \
|
||||||
|
rm -rf /tmp/* && \
|
||||||
|
apk del .build-deps
|
||||||
|
|
||||||
EXPOSE 8334
|
EXPOSE 8334
|
||||||
|
VOLUME ["/app/data/config/"]
|
||||||
WORKDIR "/app"
|
WORKDIR "/app"
|
||||||
ENV NODE_ENV production
|
CMD ["/app/nuage"]
|
||||||
CMD ["node", "/app/server/index"]
|
|
||||||
|
|
|
||||||
30
package.json
30
package.json
|
|
@ -14,33 +14,7 @@
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
"aws-sdk": "^2.59.0",
|
|
||||||
"body-parser": "^1.17.2",
|
|
||||||
"cookie-parser": "^1.4.3",
|
|
||||||
"cors": "^2.8.3",
|
|
||||||
"crypto": "0.0.3",
|
|
||||||
"express": "^4.15.3",
|
|
||||||
"express-winston": "^2.4.0",
|
|
||||||
"ftp": "^0.3.10",
|
|
||||||
"google-auth-library": "^0.10.0",
|
|
||||||
"googleapis": "^19.0.0",
|
|
||||||
"multiparty": "^4.1.3",
|
|
||||||
"node-ssh": "^4.2.2",
|
|
||||||
"nodegit": "^0.22.0",
|
|
||||||
"path": "^0.12.7",
|
|
||||||
"react-sticky": "^6.0.2",
|
|
||||||
"request": "^2.81.0",
|
|
||||||
"request-promise": "^4.2.1",
|
|
||||||
"scp2": "^0.5.0",
|
|
||||||
"ssh2-sftp-client": "^1.1.0",
|
|
||||||
"stream-to-string": "^1.1.0",
|
|
||||||
"string-to-stream": "^1.1.0",
|
|
||||||
"uglifyjs-webpack-plugin": "^1.2.5",
|
|
||||||
"webdav-fs": "^1.10.1",
|
|
||||||
"winston": "^2.3.1",
|
|
||||||
"winston-couchdb": "^0.6.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"assert": "^1.4.1",
|
"assert": "^1.4.1",
|
||||||
"babel-cli": "^6.11.4",
|
"babel-cli": "^6.11.4",
|
||||||
|
|
@ -88,12 +62,14 @@
|
||||||
"react-infinite-scroller": "^1.1.4",
|
"react-infinite-scroller": "^1.1.4",
|
||||||
"react-router": "^4.1.1",
|
"react-router": "^4.1.1",
|
||||||
"react-router-dom": "^4.1.1",
|
"react-router-dom": "^4.1.1",
|
||||||
|
"react-sticky": "^6.0.2",
|
||||||
"requirejs": "^2.3.5",
|
"requirejs": "^2.3.5",
|
||||||
"rx-lite": "^4.0.8",
|
"rx-lite": "^4.0.8",
|
||||||
"rxjs": "^5.4.0",
|
"rxjs": "^5.4.0",
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
"sass-variable-loader": "^0.1.2",
|
"sass-variable-loader": "^0.1.2",
|
||||||
"style-loader": "^0.20.2",
|
"style-loader": "^0.20.2",
|
||||||
|
"uglifyjs-webpack-plugin": "^1.2.5",
|
||||||
"url-loader": "^0.6.2",
|
"url-loader": "^0.6.2",
|
||||||
"video.js": "^5.19.2",
|
"video.js": "^5.19.2",
|
||||||
"videojs-contrib-hls": "^5.14.1",
|
"videojs-contrib-hls": "^5.14.1",
|
||||||
|
|
|
||||||
59
server/bootstrap.js
vendored
59
server/bootstrap.js
vendored
|
|
@ -1,59 +0,0 @@
|
||||||
var bodyParser = require('body-parser'),
|
|
||||||
cookieParser = require('cookie-parser'),
|
|
||||||
cors = require('cors'),
|
|
||||||
config = require('../config_server'),
|
|
||||||
express = require('express'),
|
|
||||||
winston = require('winston'),
|
|
||||||
expressWinston = require('express-winston');
|
|
||||||
|
|
||||||
require('winston-couchdb');
|
|
||||||
|
|
||||||
var app = express();
|
|
||||||
app.enable('trust proxy')
|
|
||||||
app.disable('x-powered-by');
|
|
||||||
|
|
||||||
app.use(cookieParser());
|
|
||||||
app.use(bodyParser.json());
|
|
||||||
|
|
||||||
if(process.env.NODE_ENV === 'production'){
|
|
||||||
var transports = [
|
|
||||||
new winston.transports.Console({
|
|
||||||
json: false,
|
|
||||||
colorize: false
|
|
||||||
})
|
|
||||||
];
|
|
||||||
if(config.info.usage_stats === true){
|
|
||||||
transports.push(new winston.transports.Couchdb({
|
|
||||||
host: 'log.kerjean.me',
|
|
||||||
db: 'log_nuage',
|
|
||||||
port: 443,
|
|
||||||
ssl: true,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
app.use(expressWinston.logger({
|
|
||||||
transports: transports,
|
|
||||||
requestWhitelist: [],
|
|
||||||
responseWhitelist: [],
|
|
||||||
meta: true,
|
|
||||||
exitOnError: false,
|
|
||||||
msg: "HTTP {{res.statusCode}} {{req.method}} {{req.url}} {{res.responseTime}}ms",
|
|
||||||
expressFormat: true,
|
|
||||||
colorize: false,
|
|
||||||
ignoreRoute: function (req, res) {
|
|
||||||
return /^\/api\//.test(req.originalUrl)? false : true;
|
|
||||||
},
|
|
||||||
dynamicMeta: function(req, res) {
|
|
||||||
return {
|
|
||||||
host: req.hostname,
|
|
||||||
protocol: req.protocol,
|
|
||||||
method:req.method,
|
|
||||||
pathname: req.originalUrl,
|
|
||||||
ip: req.ip,
|
|
||||||
referrer: req.get('Referrer'),
|
|
||||||
status: res.statusCode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = app;
|
|
||||||
39
server/common/app.go
Normal file
39
server/common/app.go
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
Config *Config
|
||||||
|
Helpers *Helpers
|
||||||
|
Backend IBackend
|
||||||
|
Body map[string]string
|
||||||
|
Session map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCurrentDir() string {
|
||||||
|
ex, _ := os.Executable()
|
||||||
|
return filepath.Dir(ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
var HTTPClient = http.Client{
|
||||||
|
Timeout: 5 * time.Hour,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Dial: (&net.Dialer{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
KeepAlive: 10 * time.Second,
|
||||||
|
}).Dial,
|
||||||
|
TLSHandshakeTimeout: 5 * time.Second,
|
||||||
|
IdleConnTimeout: 60 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 60 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var HTTP = http.Client{
|
||||||
|
Timeout: 800 * time.Millisecond,
|
||||||
|
}
|
||||||
50
server/common/cache.go
Normal file
50
server/common/cache.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/hashstructure"
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppCache struct {
|
||||||
|
Cache *cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppCache) Get(key interface{}) interface{} {
|
||||||
|
hash, err := hashstructure.Hash(key, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value, found := a.Cache.Get(fmt.Sprint(hash))
|
||||||
|
if found == false {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppCache) Set(key map[string]string, value interface{}) {
|
||||||
|
hash, err := hashstructure.Hash(key, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.Cache.Set(fmt.Sprint(hash), value, cache.DefaultExpiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppCache) OnEvict(fn func(string, interface{})) {
|
||||||
|
a.Cache.OnEvicted(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAppCache(arg ...time.Duration) AppCache {
|
||||||
|
var retention time.Duration = 5
|
||||||
|
var cleanup time.Duration = 10
|
||||||
|
if len(arg) > 0 {
|
||||||
|
retention = arg[0]
|
||||||
|
if len(arg) > 1 {
|
||||||
|
cleanup = arg[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c := AppCache{}
|
||||||
|
c.Cache = cache.New(retention*time.Minute, cleanup*time.Minute)
|
||||||
|
return c
|
||||||
|
}
|
||||||
188
server/common/config.go
Normal file
188
server/common/config.go
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CONFIG_PATH = "data/config/"
|
||||||
|
APP_VERSION = "v0.3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConfig() *Config {
|
||||||
|
c := Config{}
|
||||||
|
c.Initialise()
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
General struct {
|
||||||
|
Port int `json:"port"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
SecretKey string `json:"secret_key"`
|
||||||
|
Editor string `json:"editor"`
|
||||||
|
ForkButton bool `json:"fork_button"`
|
||||||
|
DisplayHidden bool `json:"display_hidden"`
|
||||||
|
} `json:"general"`
|
||||||
|
Log struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Telemetry bool `json:"telemetry"`
|
||||||
|
} `json:"log"`
|
||||||
|
OAuthProvider struct {
|
||||||
|
Dropbox struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
} `json:"dropbox"`
|
||||||
|
GoogleDrive struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
} `json:"gdrive"`
|
||||||
|
} `json:"oauth"`
|
||||||
|
Connections []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Hostname *string `json:"hostname,omitempty"`
|
||||||
|
Username *string `json:"username,omitempty"`
|
||||||
|
Password *string `json:"password,omitempty"`
|
||||||
|
Url *string `json:"url,omitempty"`
|
||||||
|
Advanced *bool `json:"advanced,omitempty"`
|
||||||
|
Port *uint `json:"port,omitempty"`
|
||||||
|
Path *string `json:"path,omitempty"`
|
||||||
|
Passphrase *string `json:"passphrase,omitempty"`
|
||||||
|
SecretAccessKey *string `json:"secret_access_key,omitempty"`
|
||||||
|
AccessKeyId *string `json:"access_key_id,omitempty"`
|
||||||
|
Endpoint *string `json:"endpoint,omitempty"`
|
||||||
|
Commit *string `json:"commit,omitempty"`
|
||||||
|
Branch *string `json:"branch,omitempty"`
|
||||||
|
AuthorEmail *string `json:"author_email,omitempty"`
|
||||||
|
AuthorName *string `json:"author_name,omitempty"`
|
||||||
|
CommitterEmail *string `json:"committer_email,omitempty"`
|
||||||
|
CommitterName *string `json:"committter_name,omitempty"`
|
||||||
|
} `json:"connections"`
|
||||||
|
Runtime struct {
|
||||||
|
Dirname string
|
||||||
|
ConfigPath string
|
||||||
|
FirstSetup bool
|
||||||
|
} `-`
|
||||||
|
MimeTypes map[string]string `json:"mimetypes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Initialise() {
|
||||||
|
c.Runtime.Dirname = GetCurrentDir()
|
||||||
|
c.Runtime.ConfigPath = filepath.Join(c.Runtime.Dirname, CONFIG_PATH)
|
||||||
|
os.MkdirAll(c.Runtime.ConfigPath, os.ModePerm)
|
||||||
|
if err := c.loadConfig(filepath.Join(c.Runtime.ConfigPath, "config.json")); err != nil {
|
||||||
|
log.Println("> Can't load configuration file")
|
||||||
|
}
|
||||||
|
if err := c.loadMimeType(filepath.Join(c.Runtime.ConfigPath, "mime.json")); err != nil {
|
||||||
|
log.Println("> Can't load mimetype config")
|
||||||
|
}
|
||||||
|
go c.ChangeListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) loadConfig(path string) error {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
defer file.Close()
|
||||||
|
if err != nil {
|
||||||
|
c = &Config{}
|
||||||
|
log.Println("can't load config file")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
err = decoder.Decode(&c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.populateDefault(path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ChangeListener() {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer watcher.Close()
|
||||||
|
done := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.Events:
|
||||||
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
|
config_path := filepath.Join(c.Runtime.ConfigPath, "config.json")
|
||||||
|
if err = c.loadConfig(config_path); err != nil {
|
||||||
|
log.Println("can't load config file")
|
||||||
|
} else {
|
||||||
|
c.populateDefault(config_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = watcher.Add(c.Runtime.ConfigPath)
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) populateDefault(path string) {
|
||||||
|
if c.General.Port == 0 {
|
||||||
|
c.General.Port = 8334
|
||||||
|
}
|
||||||
|
if c.General.SecretKey == "" {
|
||||||
|
c.General.SecretKey = RandomString(16)
|
||||||
|
j, err := json.Marshal(c)
|
||||||
|
if err == nil {
|
||||||
|
f, err := os.OpenFile(path, os.O_WRONLY, os.ModePerm)
|
||||||
|
if err == nil {
|
||||||
|
f.Write(j)
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.OAuthProvider.Dropbox.ClientID == "" {
|
||||||
|
c.OAuthProvider.Dropbox.ClientID = os.Getenv("DROPBOX_CLIENT_ID")
|
||||||
|
}
|
||||||
|
if c.OAuthProvider.GoogleDrive.ClientID == "" {
|
||||||
|
c.OAuthProvider.GoogleDrive.ClientID = os.Getenv("GDRIVE_CLIENT_ID")
|
||||||
|
}
|
||||||
|
if c.OAuthProvider.GoogleDrive.ClientSecret == "" {
|
||||||
|
c.OAuthProvider.GoogleDrive.ClientSecret = os.Getenv("GDRIVE_CLIENT_SECRET")
|
||||||
|
}
|
||||||
|
if c.General.Host == "" {
|
||||||
|
c.General.Host = os.Getenv("APPLICATION_URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Export() (string, error) {
|
||||||
|
publicConf := struct {
|
||||||
|
Editor string `json:"editor"`
|
||||||
|
ForkButton bool `json:"fork_button"`
|
||||||
|
DisplayHidden bool `json:"display_hidden"`
|
||||||
|
Connections interface{} `json:"connections"`
|
||||||
|
MimeTypes map[string]string `json:"mime"`
|
||||||
|
}{
|
||||||
|
Editor: c.General.Editor,
|
||||||
|
ForkButton: c.General.ForkButton,
|
||||||
|
DisplayHidden: c.General.DisplayHidden,
|
||||||
|
Connections: c.Connections,
|
||||||
|
MimeTypes: c.MimeTypes,
|
||||||
|
}
|
||||||
|
j, err := json.Marshal(publicConf)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(j), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) loadMimeType(path string) error {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
defer file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
return decoder.Decode(&c.MimeTypes)
|
||||||
|
}
|
||||||
109
server/common/error.go
Normal file
109
server/common/error.go
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewError(message string, status int) error {
|
||||||
|
return AppError{message, status}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppError struct {
|
||||||
|
message string
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e AppError) Error() string {
|
||||||
|
return fmt.Sprintf("%s", e.message)
|
||||||
|
}
|
||||||
|
func (e AppError) Status() int {
|
||||||
|
return e.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func HTTPFriendlyStatus(n int) string {
|
||||||
|
if n < 400 && n > 600 {
|
||||||
|
return "Humm"
|
||||||
|
}
|
||||||
|
switch n {
|
||||||
|
case 400:
|
||||||
|
return "Bad Request"
|
||||||
|
case 401:
|
||||||
|
return "Unauthorized"
|
||||||
|
case 402:
|
||||||
|
return "Payment Required"
|
||||||
|
case 403:
|
||||||
|
return "Forbidden"
|
||||||
|
case 404:
|
||||||
|
return "Not Found"
|
||||||
|
case 405:
|
||||||
|
return "Not Allowed"
|
||||||
|
case 406:
|
||||||
|
return "Not Acceptable"
|
||||||
|
case 407:
|
||||||
|
return "Authentication Required"
|
||||||
|
case 408:
|
||||||
|
return "Timeout"
|
||||||
|
case 409:
|
||||||
|
return "Conflict"
|
||||||
|
case 410:
|
||||||
|
return "Gone"
|
||||||
|
case 411:
|
||||||
|
return "Length Required"
|
||||||
|
case 412:
|
||||||
|
return "Failed"
|
||||||
|
case 413:
|
||||||
|
return "Too Large"
|
||||||
|
case 414:
|
||||||
|
return "URI Too Long"
|
||||||
|
case 415:
|
||||||
|
return "Unsupported Media"
|
||||||
|
case 416:
|
||||||
|
return "Not Like This"
|
||||||
|
case 417:
|
||||||
|
return "Unexpected"
|
||||||
|
case 418:
|
||||||
|
return "I'm a teapot"
|
||||||
|
case 421:
|
||||||
|
return "Redirection Problem"
|
||||||
|
case 422:
|
||||||
|
return "Unprocessable"
|
||||||
|
case 423:
|
||||||
|
return "Locked"
|
||||||
|
case 424:
|
||||||
|
return "Failed Dependency"
|
||||||
|
case 426:
|
||||||
|
return "Upgrade Required"
|
||||||
|
case 428:
|
||||||
|
return "Need Something"
|
||||||
|
case 429:
|
||||||
|
return "Too Many Requests"
|
||||||
|
case 431:
|
||||||
|
return "Request Too Large"
|
||||||
|
case 451:
|
||||||
|
return "Not Available"
|
||||||
|
case 500:
|
||||||
|
return "Internal Server Error"
|
||||||
|
case 501:
|
||||||
|
return "Not Implemented"
|
||||||
|
case 502:
|
||||||
|
return "Bad Gateway"
|
||||||
|
case 503:
|
||||||
|
return "Service Unavailable"
|
||||||
|
case 504:
|
||||||
|
return "Gateway Timeout"
|
||||||
|
case 505:
|
||||||
|
return "Unsupported HTTP Version"
|
||||||
|
case 506:
|
||||||
|
return "Need To Negotiate"
|
||||||
|
case 507:
|
||||||
|
return "Insufficient Storage"
|
||||||
|
case 508:
|
||||||
|
return "Loop Detected"
|
||||||
|
case 510:
|
||||||
|
return "Not Extended"
|
||||||
|
case 511:
|
||||||
|
return "Authentication Required"
|
||||||
|
default:
|
||||||
|
return "Oops"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
server/common/files.go
Normal file
8
server/common/files.go
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
func IsDirectory(path string) bool {
|
||||||
|
if string(path[len(path)-1]) != "/" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
39
server/common/helpers.go
Normal file
39
server/common/helpers.go
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Helpers struct {
|
||||||
|
AbsolutePath func(p string) string
|
||||||
|
MimeType func(p string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHelpers(config *Config) *Helpers {
|
||||||
|
return &Helpers{
|
||||||
|
MimeType: mimeType(config),
|
||||||
|
AbsolutePath: absolutePath(config),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func absolutePath(c *Config) func(p string) string {
|
||||||
|
return func(p string) string {
|
||||||
|
return filepath.Join(c.Runtime.Dirname, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mimeType(c *Config) func(p string) string {
|
||||||
|
return func(p string) string {
|
||||||
|
ext := filepath.Ext(p)
|
||||||
|
if ext != "" {
|
||||||
|
ext = ext[1:]
|
||||||
|
}
|
||||||
|
ext = strings.ToLower(ext)
|
||||||
|
mType := c.MimeTypes[ext]
|
||||||
|
if mType == "" {
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
return mType
|
||||||
|
}
|
||||||
|
}
|
||||||
29
server/common/log.go
Normal file
29
server/common/log.go
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogEntry struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
RequestURI string `json:"pathname"`
|
||||||
|
Proto string `json:"proto"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
|
Ip string `json:"ip"`
|
||||||
|
Referer string `json:"referer"`
|
||||||
|
Timestamp time.Time `json:"_id"`
|
||||||
|
Duration int64 `json:"responseTime"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Backend string `json:"backend"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug_reader(r io.Reader) {
|
||||||
|
a, _ := ioutil.ReadAll(r)
|
||||||
|
fmt.Println("> DEBUG:", string(a))
|
||||||
|
}
|
||||||
60
server/common/types.go
Normal file
60
server/common/types.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IBackend interface {
|
||||||
|
Ls(path string) ([]os.FileInfo, error)
|
||||||
|
Cat(path string) (io.Reader, error)
|
||||||
|
Mkdir(path string) error
|
||||||
|
Rm(path string) error
|
||||||
|
Mv(from string, to string) error
|
||||||
|
Save(path string, file io.Reader) error
|
||||||
|
Touch(path string) error
|
||||||
|
Info() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
FName string `json:"name"`
|
||||||
|
FType string `json:"type"`
|
||||||
|
FTime int64 `json:"time"`
|
||||||
|
FSize int64 `json:"size"`
|
||||||
|
CanRename *bool `json:"can_rename,omitempty"`
|
||||||
|
CanMove *bool `json:"can_move_directory,omitempty"`
|
||||||
|
CanDelete *bool `json:"can_delete,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) Name() string {
|
||||||
|
return f.FName
|
||||||
|
}
|
||||||
|
func (f File) Size() int64 {
|
||||||
|
return f.FSize
|
||||||
|
}
|
||||||
|
func (f File) Mode() os.FileMode {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
func (f File) ModTime() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
func (f File) IsDir() bool {
|
||||||
|
if f.FType != "directory" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (f File) Sys() interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
CanSee *bool `json:"can_read,omitempty"`
|
||||||
|
CanCreateFile *bool `json:"can_create_file,omitempty"`
|
||||||
|
CanCreateDirectory *bool `json:"can_create_directory,omitempty"`
|
||||||
|
CanRename *bool `json:"can_rename,omitempty"`
|
||||||
|
CanMove *bool `json:"can_move,omitempty"`
|
||||||
|
CanUpload *bool `json:"can_upload,omitempty"`
|
||||||
|
Expire *time.Time `json:"-"`
|
||||||
|
}
|
||||||
19
server/common/utils.go
Normal file
19
server/common/utils.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||||
|
|
||||||
|
func RandomString(n int) string {
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = Letters[rand.Intn(len(Letters))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBool(t bool) *bool {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
var express = require('express'),
|
|
||||||
app = express.Router(),
|
|
||||||
path = require('path'),
|
|
||||||
crypto = require('../utils/crypto'),
|
|
||||||
Files = require('../model/files'),
|
|
||||||
config = require('../../config_server'),
|
|
||||||
multiparty = require('multiparty'),
|
|
||||||
request = require('request'),
|
|
||||||
FormData = require('form-data'),
|
|
||||||
mime = require('../utils/mimetype.js');
|
|
||||||
|
|
||||||
app.use(function(req, res, next){
|
|
||||||
req.cookies.auth = crypto.decrypt(req.cookies.auth);
|
|
||||||
if(req.cookies.auth !== null){
|
|
||||||
return next();
|
|
||||||
}else{
|
|
||||||
return res.status(401).send({status: "error", message: "You need to authenticate first"});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// list files
|
|
||||||
app.get('/ls', function(req, res){
|
|
||||||
const path = pathBuilder(req);
|
|
||||||
if(path){
|
|
||||||
Files
|
|
||||||
.ls(path, req.cookies.auth)
|
|
||||||
.then(function(results){ res.send({status: 'ok', results: results}); })
|
|
||||||
.catch(function(err){ errorHandler(res, err, 'cannot fetch files'); });
|
|
||||||
}else{
|
|
||||||
res.send({status: 'error', message: 'unknown path'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// get a file content
|
|
||||||
app.get('/cat', function(req, res){
|
|
||||||
const path = pathBuilder(req);
|
|
||||||
res.clearCookie("download");
|
|
||||||
if(path){
|
|
||||||
Files.cat(path, req.cookies.auth, res)
|
|
||||||
.then(function(_stream){
|
|
||||||
_stream = _stream.on('error', function (error) {
|
|
||||||
let status = 404;
|
|
||||||
if(error && typeof error.status === "number"){
|
|
||||||
status = error.status;
|
|
||||||
}
|
|
||||||
res.status(status).send({status: status, message: "There's nothing here"});
|
|
||||||
if(typeof this.end === "function") this.end();
|
|
||||||
});
|
|
||||||
const mType = mime.getMimeType(path);
|
|
||||||
res.set('Content-Type', mType);
|
|
||||||
|
|
||||||
if(!config.transcoder.url){ return _stream.pipe(res); }
|
|
||||||
|
|
||||||
if(/^image\//.test(mType) && ["image/gif", "image/svg", "image/x-icon"].indexOf(mType) === -1){
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('image', _stream, {
|
|
||||||
filename: 'tmp',
|
|
||||||
contentType: mType,
|
|
||||||
});
|
|
||||||
|
|
||||||
let endpoint = config.transcoder.url;
|
|
||||||
if(req.query.size){
|
|
||||||
endpoint += "?size="+req.query.size+"&meta="+(req.query.meta === "true" ? "true": "false");
|
|
||||||
}
|
|
||||||
const post_request = request({
|
|
||||||
method: "POST",
|
|
||||||
url: endpoint,
|
|
||||||
headers: form.getHeaders()
|
|
||||||
});
|
|
||||||
return form.pipe(post_request)
|
|
||||||
.on('error', (err) => {
|
|
||||||
res.status(500).end();
|
|
||||||
})
|
|
||||||
.pipe(res);
|
|
||||||
}
|
|
||||||
return _stream.pipe(res);
|
|
||||||
})
|
|
||||||
.catch(function(err){ errorHandler(res, err, 'couldn\'t read the file'); });
|
|
||||||
}else{
|
|
||||||
res.send({status: 'error', message: 'unknown path'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// create/update a file
|
|
||||||
// https://github.com/pillarjs/multiparty
|
|
||||||
app.post('/cat', function(req, res){
|
|
||||||
const form = new multiparty.Form(),
|
|
||||||
path = pathBuilder(req);
|
|
||||||
|
|
||||||
if(path){
|
|
||||||
form.on('part', function(part) {
|
|
||||||
part.on('error', function(err){
|
|
||||||
errorHandler(res, {code: "INTERNAL_ERROR", message: "internal error"}, 'internal error');
|
|
||||||
});
|
|
||||||
|
|
||||||
Files.write(path, part, req.cookies.auth)
|
|
||||||
.then(function(result){
|
|
||||||
res.send({status: 'ok'});
|
|
||||||
})
|
|
||||||
.catch(function(err){ errorHandler(res, err, 'couldn\'t write the file'); });
|
|
||||||
});
|
|
||||||
form.parse(req);
|
|
||||||
}else{
|
|
||||||
res.send({status: 'error', message: 'unknown path'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// rename a file/directory
|
|
||||||
app.get('/mv', function(req, res){
|
|
||||||
req.query.path = req.query.from;
|
|
||||||
const from = pathBuilder(req);
|
|
||||||
req.query.path = req.query.to;
|
|
||||||
const to = pathBuilder(req)
|
|
||||||
|
|
||||||
if(from === to){
|
|
||||||
res.send({status: 'ok'});
|
|
||||||
}else if(from && to){
|
|
||||||
Files.mv(from, to, req.cookies.auth)
|
|
||||||
.then(function(message){ res.send({status: 'ok'}); })
|
|
||||||
.catch(function(err){ errorHandler(res, err, 'couldn\'t rename your file'); });
|
|
||||||
}else{
|
|
||||||
res.send({status: 'error', message: 'unknown path'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// delete a file/directory
|
|
||||||
app.get('/rm', function(req, res){
|
|
||||||
const path = pathBuilder(req);
|
|
||||||
if(path){
|
|
||||||
Files.rm(path, req.cookies.auth)
|
|
||||||
.then(function(message){ res.send({status: 'ok'}); })
|
|
||||||
.catch(function(err){ errorHandler(res, err, 'couldn\'t delete your file'); });
|
|
||||||
}else{
|
|
||||||
res.send({status: 'error', message: 'unknown path'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// create a directory
|
|
||||||
app.get('/mkdir', function(req, res){
|
|
||||||
const path = pathBuilder(req);
|
|
||||||
if(path){
|
|
||||||
Files.mkdir(path, req.cookies.auth)
|
|
||||||
.then(function(message){ res.send({status: 'ok'}); })
|
|
||||||
.catch(function(err){ errorHandler(res, err, 'couldn\'t create the directory'); });
|
|
||||||
}else{
|
|
||||||
res.send({status: 'error', message: 'unknown path'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/touch', function(req, res){
|
|
||||||
const path = pathBuilder(req);
|
|
||||||
if(path){
|
|
||||||
Files.touch(path, req.cookies.auth)
|
|
||||||
.then(function(message){ res.send({status: 'ok'}); })
|
|
||||||
.catch(function(err){ errorHandler(res, err, 'couldn\'t create the file'); });
|
|
||||||
}else{
|
|
||||||
res.send({status: 'error', message: 'unknown path'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = app;
|
|
||||||
|
|
||||||
function pathBuilder(req){
|
|
||||||
return path.posix.join(req.cookies.auth.payload.path || '', decodeURIComponent(req.query.path) || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorHandler(res, err, defaultMessage){
|
|
||||||
const code = {
|
|
||||||
"INTERNAL_ERROR": {message: "Oops, it seems we had a problem", status: 500},
|
|
||||||
"ECONNREFUSED": {message: "Oops, the service you are connected on is not available", status: 502}
|
|
||||||
};
|
|
||||||
const status = function(_code, _status){
|
|
||||||
if(code[_code]){
|
|
||||||
return code[_code]['status'];
|
|
||||||
}
|
|
||||||
_status = parseInt(_status);
|
|
||||||
if(_status >= 400 && _status < 600){
|
|
||||||
return _status;
|
|
||||||
}
|
|
||||||
return 404;
|
|
||||||
}(err.code || err.errno, err.status);
|
|
||||||
|
|
||||||
if(code[err.code || err.errno]){
|
|
||||||
res.status(status).send({
|
|
||||||
status: 'error',
|
|
||||||
message: code[err.code]['message']
|
|
||||||
});
|
|
||||||
}else if(err.message){
|
|
||||||
res.status(status).send({
|
|
||||||
status: 'error',
|
|
||||||
message: err.message || 'cannot fetch files',
|
|
||||||
trace: err
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
res.status(status).send({
|
|
||||||
status: 'error',
|
|
||||||
message: defaultMessage,
|
|
||||||
trace: err
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
var express = require('express'),
|
|
||||||
app = express.Router(),
|
|
||||||
crypto = require('../utils/crypto'),
|
|
||||||
Session = require('../model/session'),
|
|
||||||
http = require('request-promise');
|
|
||||||
|
|
||||||
app.get('/', function(req, res){
|
|
||||||
let data = crypto.decrypt(req.cookies.auth);
|
|
||||||
if(data && data.type){
|
|
||||||
res.send({status: 'ok', result: true})
|
|
||||||
}else{
|
|
||||||
res.send({status: 'ok', result: false})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/', function(req, res){
|
|
||||||
Session.test(req.body)
|
|
||||||
.then((state) => {
|
|
||||||
if(!state.path) state.path = "";
|
|
||||||
else{ state.path = state.path.replace(/\/$/, ''); }
|
|
||||||
let persist = {
|
|
||||||
type: req.body.type,
|
|
||||||
payload: state
|
|
||||||
};
|
|
||||||
const cookie = crypto.encrypt(persist);
|
|
||||||
if(Buffer.byteLength(cookie, 'utf-8') > 4096){
|
|
||||||
res.status(413).send({status: 'error', message: 'we can\'t authenticate you'})
|
|
||||||
}else{
|
|
||||||
res.cookie('auth', crypto.encrypt(persist), { maxAge: 365*24*60*60*1000, httpOnly: true, path: "/api/" });
|
|
||||||
res.send({status: 'ok'});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
let message = function(err){
|
|
||||||
let t = err && err.message || 'could not establish a connection';
|
|
||||||
if(err.code){
|
|
||||||
t += ' ('+err.code+')';
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
res.status(401).send({status: 'error', message: message(err), code: err.code});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete('/', function(req, res){
|
|
||||||
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'});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/auth/:id', function(req, res){
|
|
||||||
Session.auth({type: req.params.id})
|
|
||||||
.then((url) => {
|
|
||||||
res.send({status: 'ok', result: url});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
res.status(404).send({status: 'error', message: 'can\'t get authorization url', trace: err});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = app;
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
var app = require('./bootstrap'),
|
|
||||||
express = require('express'),
|
|
||||||
filesRouter = require('./ctrl/files'),
|
|
||||||
sessionRouter = require('./ctrl/session'),
|
|
||||||
fs = require('fs');
|
|
||||||
|
|
||||||
|
|
||||||
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/'))
|
|
||||||
app.use('/*', function (req, res){
|
|
||||||
fs.createReadStream(__dirname + '/public/index.html').pipe(res);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(8334, function(err){
|
|
||||||
if(err){ console.log(err); }
|
|
||||||
else{ console.log("Running: http://127.0.0.1:8334"); }
|
|
||||||
});
|
|
||||||
50
server/main.go
Normal file
50
server/main.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
//"context"
|
||||||
|
//"github.com/getlantern/systray"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"github.com/mickael-kerjean/nuage/server/router"
|
||||||
|
//"github.com/pkg/browser"
|
||||||
|
//"io/ioutil"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var APP_URL string
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := App{}
|
||||||
|
app.Config = NewConfig()
|
||||||
|
app.Helpers = NewHelpers(app.Config)
|
||||||
|
router.Init(&app)
|
||||||
|
|
||||||
|
APP_URL = "http://" + app.Config.General.Host + ":" + strconv.Itoa(app.Config.General.Port)
|
||||||
|
// systray.Run(setupSysTray(&app), func() {
|
||||||
|
// srv.Shutdown(context.TODO())
|
||||||
|
// })
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func setupSysTray(a *App) func() {
|
||||||
|
// return func() {
|
||||||
|
// b, err := ioutil.ReadFile(a.Config.Runtime.AbsolutePath("data/public/assets/logo/favicon.ico"))
|
||||||
|
// if err != nil {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// systray.SetIcon(b)
|
||||||
|
// mOpen := systray.AddMenuItem("Open", "Open in a browser")
|
||||||
|
// mQuit := systray.AddMenuItem("Quit", "Quit the whole app")
|
||||||
|
|
||||||
|
// go func() {
|
||||||
|
// for {
|
||||||
|
// select {
|
||||||
|
// case <-mOpen.ClickedCh:
|
||||||
|
// browser.OpenURL(APP_URL)
|
||||||
|
// case <-mQuit.ClickedCh:
|
||||||
|
// systray.Quit()
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
201
server/model/backend/dropbox.go
Normal file
201
server/model/backend/dropbox.go
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dropbox struct {
|
||||||
|
ClientId string
|
||||||
|
Hostname string
|
||||||
|
Bearer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDropbox(params map[string]string, app *App) (IBackend, error) {
|
||||||
|
backend := Dropbox{}
|
||||||
|
backend.ClientId = app.Config.OAuthProvider.Dropbox.ClientID
|
||||||
|
backend.Hostname = app.Config.General.Host
|
||||||
|
backend.Bearer = params["bearer"]
|
||||||
|
|
||||||
|
if backend.ClientId == "" {
|
||||||
|
return backend, NewError("Missing ClientID: Contact your admin", 502)
|
||||||
|
} else if backend.Hostname == "" {
|
||||||
|
return backend, NewError("Missing Hostname: Contact your admin", 502)
|
||||||
|
}
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) Info() string {
|
||||||
|
return "dropbox"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) OAuthURL() string {
|
||||||
|
url := "https://www.dropbox.com/oauth2/authorize?"
|
||||||
|
url += "client_id=" + d.ClientId
|
||||||
|
url += "&redirect_uri=" + d.Hostname + "/login"
|
||||||
|
url += "&response_type=token"
|
||||||
|
url += "&state=dropbox"
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) Ls(path string) ([]os.FileInfo, error) {
|
||||||
|
files := make([]os.FileInfo, 0)
|
||||||
|
|
||||||
|
args := struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Recursive bool `json:"recursive"`
|
||||||
|
IncludeDeleted bool `json:"include_deleted"`
|
||||||
|
IncludeMediaInfo bool `json:"include_media_info"`
|
||||||
|
}{d.path(path), false, false, true}
|
||||||
|
res, err := d.request("POST", "https://api.dropboxapi.com/2/files/list_folder", d.toReader(args), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return nil, NewError(HTTPFriendlyStatus(res.StatusCode)+": can't get things in"+filepath.Base(path), res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r struct {
|
||||||
|
Files []struct {
|
||||||
|
Type string `json:".tag"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Time time.Time `json:"client_modified"`
|
||||||
|
Size uint `json:"size"`
|
||||||
|
} `json:"entries"`
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(res.Body)
|
||||||
|
decoder.Decode(&r)
|
||||||
|
|
||||||
|
for _, obj := range r.Files {
|
||||||
|
files = append(files, File{
|
||||||
|
FName: obj.Name,
|
||||||
|
FType: func(p string) string {
|
||||||
|
if p == "folder" {
|
||||||
|
return "directory"
|
||||||
|
}
|
||||||
|
return "file"
|
||||||
|
}(obj.Type),
|
||||||
|
FTime: obj.Time.UnixNano() / 1000,
|
||||||
|
FSize: int64(obj.Size),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) Cat(path string) (io.Reader, error) {
|
||||||
|
res, err := d.request("POST", "https://content.dropboxapi.com/2/files/download", nil, func(req *http.Request) {
|
||||||
|
arg := struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{d.path(path)}
|
||||||
|
json, _ := ioutil.ReadAll(d.toReader(arg))
|
||||||
|
req.Header.Set("Dropbox-API-Arg", string(json))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) Mkdir(path string) error {
|
||||||
|
args := struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Autorename bool `json:"autorename"`
|
||||||
|
}{d.path(path), false}
|
||||||
|
res, err := d.request("POST", "https://api.dropboxapi.com/2/files/create_folder_v2", d.toReader(args), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't create "+filepath.Base(path), res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) Rm(path string) error {
|
||||||
|
args := struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{d.path(path)}
|
||||||
|
res, err := d.request("POST", "https://api.dropboxapi.com/2/files/delete_v2", d.toReader(args), nil)
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't remove "+filepath.Base(path), res.StatusCode)
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) Mv(from string, to string) error {
|
||||||
|
args := struct {
|
||||||
|
FromPath string `json:"from_path"`
|
||||||
|
ToPath string `json:"to_path"`
|
||||||
|
}{d.path(from), d.path(to)}
|
||||||
|
res, err := d.request("POST", "https://api.dropboxapi.com/2/files/move_v2", d.toReader(args), nil)
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't do that", res.StatusCode)
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) Touch(path string) error {
|
||||||
|
return d.Save(path, strings.NewReader(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) Save(path string, file io.Reader) error {
|
||||||
|
res, err := d.request("POST", "https://content.dropboxapi.com/2/files/upload", file, func(req *http.Request) {
|
||||||
|
arg := struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
AutoRename bool `json:"autorename"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
}{d.path(path), false, "overwrite"}
|
||||||
|
json, _ := ioutil.ReadAll(d.toReader(arg))
|
||||||
|
req.Header.Set("Dropbox-API-Arg", string(json))
|
||||||
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't do that", res.StatusCode)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) request(method string, url string, body io.Reader, fn func(*http.Request)) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+d.Bearer)
|
||||||
|
if fn == nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
} else {
|
||||||
|
fn(req)
|
||||||
|
}
|
||||||
|
if req.Body != nil {
|
||||||
|
defer req.Body.Close()
|
||||||
|
}
|
||||||
|
return HTTPClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) toReader(a interface{}) io.Reader {
|
||||||
|
j, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return strings.NewReader(string(j))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Dropbox) path(path string) string {
|
||||||
|
return regexp.MustCompile(`\/$`).ReplaceAllString(path, "")
|
||||||
|
}
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
// doc: https://www.dropbox.com/developers/documentation/http/documentation
|
|
||||||
|
|
||||||
var http = require('request-promise'),
|
|
||||||
http_stream = require('request'),
|
|
||||||
Path = require('path'),
|
|
||||||
config = require('../../../config_server'),
|
|
||||||
toString = require('stream-to-string'),
|
|
||||||
Readable = require('stream').Readable;
|
|
||||||
|
|
||||||
function query(params, uri, method = 'GET', data, opts = {}){
|
|
||||||
if(!opts.headers) opts.headers = {};
|
|
||||||
opts.headers['Authorization'] = 'Bearer '+params.bearer;
|
|
||||||
opts.uri = uri;
|
|
||||||
opts.method = method;
|
|
||||||
if(data && typeof data === 'object'){
|
|
||||||
opts.body = JSON.stringify(data);
|
|
||||||
opts.headers["Content-Type"] = "application/json";
|
|
||||||
}
|
|
||||||
return http(opts)
|
|
||||||
.then((res) => Promise.resolve(JSON.parse(res)))
|
|
||||||
.catch((res) => {
|
|
||||||
if(res && res.response && res.response.body){
|
|
||||||
return Promise.reject(res.response.body);
|
|
||||||
}else{
|
|
||||||
return Promise.reject(res);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function query_stream(params, uri, method = 'GET', data, opts = {}){
|
|
||||||
if(!opts.headers) opts.headers = {};
|
|
||||||
opts.headers['Authorization'] = 'Bearer '+params.bearer;
|
|
||||||
opts.uri = uri;
|
|
||||||
opts.method = method;
|
|
||||||
opts.body = data;
|
|
||||||
return Promise.resolve(http_stream(opts));
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
auth: function(params){
|
|
||||||
let url = "https://www.dropbox.com/oauth2/authorize?client_id="+config.dropbox.clientID+"&response_type=token&redirect_uri="+config.dropbox.redirectURI+"&state=dropbox"
|
|
||||||
return Promise.resolve(url)
|
|
||||||
},
|
|
||||||
test: function(params){
|
|
||||||
return query(params, "https://api.dropboxapi.com/2/users/get_current_account", "POST")
|
|
||||||
.then((opts) => Promise.resolve(params))
|
|
||||||
.catch((err) => Promise.reject({message: 'Dropbox didn\'t gave us access to your account', code: "NOT_AUTHENTICATED"}))
|
|
||||||
},
|
|
||||||
cat: function(path, params){
|
|
||||||
return query_stream(params, "https://content.dropboxapi.com/2/files/download", "POST", null, {
|
|
||||||
headers: {
|
|
||||||
"Dropbox-API-Arg": JSON.stringify({path: path})
|
|
||||||
}
|
|
||||||
}).then((res) => {
|
|
||||||
// dropbox send silly mimetype like 'application/octet-stream' for pdf files ...
|
|
||||||
// We can't trust them on this, so we get rid of it. In our case, it will be set by the file controller
|
|
||||||
const newRes = res.on('response', function(res) {
|
|
||||||
delete res.headers['content-type'];
|
|
||||||
});
|
|
||||||
return Promise.resolve(newRes);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
ls: function(path, params){
|
|
||||||
if(path === '/') path = '';
|
|
||||||
return query(params, "https://api.dropboxapi.com/2/files/list_folder", "POST", {path: path, recursive: false, include_deleted: false, include_media_info: true})
|
|
||||||
.then((res) => {
|
|
||||||
let files = res.entries.map((file) => {
|
|
||||||
let tmp = {
|
|
||||||
size: file.size,
|
|
||||||
time: new Date(file.client_modified).getTime(),
|
|
||||||
type: file['.tag'] === 'file' ? 'file' : 'directory',
|
|
||||||
name: file.name
|
|
||||||
};
|
|
||||||
return tmp;
|
|
||||||
});
|
|
||||||
return Promise.resolve(files);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
write: function(path, content, params){
|
|
||||||
return write_file(path, content, params);
|
|
||||||
},
|
|
||||||
rm: function(path, params){
|
|
||||||
return query(params, "https://api.dropboxapi.com/2/files/delete_v2", "POST", {path: path})
|
|
||||||
.then((res) => Promise.resolve('ok'));
|
|
||||||
},
|
|
||||||
mv: function(from, to, params){
|
|
||||||
return query(params, "https://api.dropboxapi.com/2/files/move_v2", "POST", {from_path: from, to_path: to})
|
|
||||||
.then((res) => verifyDropbox(res, to, params, 10))
|
|
||||||
.catch(err => Promise.reject({message: JSON.parse(err).error, code: "DROPBOX_MOVE"}));
|
|
||||||
},
|
|
||||||
mkdir: function(path, params){
|
|
||||||
path = path.replace(/\/$/, '');
|
|
||||||
return query(params, "https://api.dropboxapi.com/2/files/create_folder_v2", "POST", {path: path, autorename: false})
|
|
||||||
.then((res) => verifyDropbox(res, path, params, 10))
|
|
||||||
.then((res) => Promise.resolve('ok'));
|
|
||||||
},
|
|
||||||
touch: function(path, params){
|
|
||||||
var stream = new Readable(); stream.push(''); stream.push(null);
|
|
||||||
return write_file(path, stream, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function write_file(path, content, params){
|
|
||||||
return process(path, content, params)
|
|
||||||
.then((res) => retryOnError(res, path, content, params, 5))
|
|
||||||
.then((res) => verifyDropbox(res, path, params, 10))
|
|
||||||
|
|
||||||
function process(path, content, params){
|
|
||||||
return query_stream(params, "https://content.dropboxapi.com/2/files/upload", "POST", content, {
|
|
||||||
headers: {
|
|
||||||
"Dropbox-API-Arg": JSON.stringify({
|
|
||||||
path: path,
|
|
||||||
autorename: false,
|
|
||||||
mode: "overwrite"
|
|
||||||
}),
|
|
||||||
"Content-Type": "application/octet-stream"
|
|
||||||
}
|
|
||||||
}).then(toString)
|
|
||||||
}
|
|
||||||
function retryOnError(body, path, content, params, n = 5){
|
|
||||||
body = JSON.parse(body);
|
|
||||||
if(body && body.error){
|
|
||||||
return sleep(Math.abs(5 - n) * 1000)
|
|
||||||
.then(() => process(path, content, params, n -1))
|
|
||||||
}else{
|
|
||||||
return Promise.resolve(body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function verifyDropbox(keep, path, params, n = 10){
|
|
||||||
let folder_path = Path.posix.dirname(path).replace(/\/$/, '');
|
|
||||||
if(folder_path === '.'){
|
|
||||||
folder_path = '';
|
|
||||||
}
|
|
||||||
return sleep(Math.abs(10 - n) * 300)
|
|
||||||
.then(() => query(params, "https://api.dropboxapi.com/2/files/list_folder", "POST", {path: folder_path, recursive: false, include_deleted: false, include_media_info: true}))
|
|
||||||
.then((res) => {
|
|
||||||
let found = res.entries.find((function(file){
|
|
||||||
return file.path_display === path? true : false
|
|
||||||
}));
|
|
||||||
if(found){
|
|
||||||
return Promise.resolve(keep)
|
|
||||||
}else{
|
|
||||||
if(n > 0){
|
|
||||||
return verifyDropbox(keep, path, params, n - 1)
|
|
||||||
}else{
|
|
||||||
return Promise.reject({message: 'dropbox didn\' create the file or was taking too long to do so', code: 'DROPBOX_WRITE_ERROR'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function sleep(t=1000, arg){
|
|
||||||
return new Promise((done) => {
|
|
||||||
setTimeout(function(){
|
|
||||||
done(arg);
|
|
||||||
}, t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
158
server/model/backend/ftp.go
Normal file
158
server/model/backend/ftp.go
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"github.com/secsy/goftp"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FtpCache AppCache
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
FtpCache = NewAppCache(2, 1)
|
||||||
|
FtpCache.OnEvict(func(key string, value interface{}) {
|
||||||
|
c := value.(*Ftp)
|
||||||
|
c.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ftp struct {
|
||||||
|
client *goftp.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFtp(params map[string]string, app *App) (IBackend, error) {
|
||||||
|
c := FtpCache.Get(params)
|
||||||
|
if c != nil {
|
||||||
|
d := c.(*Ftp)
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
if params["hostname"] == "" {
|
||||||
|
params["hostname"] = "localhost"
|
||||||
|
}
|
||||||
|
if params["port"] == "" {
|
||||||
|
params["port"] = "21"
|
||||||
|
}
|
||||||
|
if params["username"] == "" {
|
||||||
|
params["username"] = "anonymous"
|
||||||
|
}
|
||||||
|
if params["username"] == "anonymous" && params["password"] == "" {
|
||||||
|
params["password"] = "anonymous"
|
||||||
|
}
|
||||||
|
|
||||||
|
config := goftp.Config{
|
||||||
|
User: params["username"],
|
||||||
|
Password: params["password"],
|
||||||
|
ConnectionsPerHost: 2,
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
client, err := goftp.DialConfig(config, params["hostname"]+":"+params["port"])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
backend := Ftp{client}
|
||||||
|
|
||||||
|
FtpCache.Set(params, &backend)
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Info() string {
|
||||||
|
return "ftp"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Home() (string, error) {
|
||||||
|
return f.client.Getwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Ls(path string) ([]os.FileInfo, error) {
|
||||||
|
// by default FTP don't seem to mind a readdir on a non existing
|
||||||
|
// directory so we first need to make sure the directory exists
|
||||||
|
conn, err := f.client.OpenRawConn()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
i, s, err := conn.SendCommand("CWD %s", path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError(err.Error(), 404)
|
||||||
|
} else if i >= 300 {
|
||||||
|
return nil, NewError(s, 404)
|
||||||
|
}
|
||||||
|
return f.client.ReadDir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Cat(path string) (io.Reader, error) {
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
if err := f.client.Retrieve(path, pw); err != nil {
|
||||||
|
pr.CloseWithError(NewError("Problem", 409))
|
||||||
|
}
|
||||||
|
pw.Close()
|
||||||
|
}()
|
||||||
|
return pr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Mkdir(path string) error {
|
||||||
|
_, err := f.client.Mkdir(path)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Rm(path string) error {
|
||||||
|
isDirectory := func(p string) bool {
|
||||||
|
return regexp.MustCompile(`\/$`).MatchString(p)
|
||||||
|
}
|
||||||
|
transformError := func(e error) error {
|
||||||
|
// For some reasons bsftp is struggling with the library
|
||||||
|
// sometimes returning a 200 OK
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if obj, ok := e.(goftp.Error); ok {
|
||||||
|
if obj.Code() < 300 && obj.Code() > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if isDirectory(path) {
|
||||||
|
entries, err := f.Ls(path)
|
||||||
|
if transformError(err) != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
err = f.Rm(path + entry.Name() + "/")
|
||||||
|
if transformError(err) != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = f.Rm(path + entry.Name())
|
||||||
|
if transformError(err) != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = f.client.Rmdir(path)
|
||||||
|
return transformError(err)
|
||||||
|
}
|
||||||
|
err := f.client.Delete(path)
|
||||||
|
return transformError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Mv(from string, to string) error {
|
||||||
|
return f.client.Rename(from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Touch(path string) error {
|
||||||
|
return f.client.Store(path, strings.NewReader(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Save(path string, file io.Reader) error {
|
||||||
|
return f.client.Store(path, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Ftp) Close() error {
|
||||||
|
return f.client.Close()
|
||||||
|
}
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
var FtpClient = require("ftp");
|
|
||||||
|
|
||||||
// connections are reused to make things faster and avoid too much problems
|
|
||||||
const connections = {};
|
|
||||||
setInterval(() => {
|
|
||||||
for(let key in connections){
|
|
||||||
if(connections[key].date + (1000*120) < new Date().getTime()){
|
|
||||||
connections[key].conn.end();
|
|
||||||
delete connections[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
function connect(params){
|
|
||||||
if(connections[JSON.stringify(params)]){
|
|
||||||
connections[JSON.stringify(params)].date = new Date().getTime();
|
|
||||||
return Promise.resolve(connections[JSON.stringify(params)].conn);
|
|
||||||
}else{
|
|
||||||
let c = new FtpClient();
|
|
||||||
c.connect({
|
|
||||||
host: params.hostname,
|
|
||||||
port: params.port || 21,
|
|
||||||
user: params.username,
|
|
||||||
password: params.password
|
|
||||||
});
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
c.on('ready', function(){
|
|
||||||
clearTimeout(timeout);
|
|
||||||
done(c);
|
|
||||||
connections[JSON.stringify(params)] = {
|
|
||||||
date: new Date().getTime(),
|
|
||||||
conn: c
|
|
||||||
}
|
|
||||||
});
|
|
||||||
c.on('error', function(error){
|
|
||||||
err(error)
|
|
||||||
})
|
|
||||||
// because of: https://github.com/mscdex/node-ftp/issues/187
|
|
||||||
let timeout = setTimeout(() => {
|
|
||||||
err('timeout');
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
test: function(params){
|
|
||||||
return connect(params)
|
|
||||||
.then(() => Promise.resolve(params))
|
|
||||||
},
|
|
||||||
cat: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((c) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
c.get(path, function(error, stream) {
|
|
||||||
if (error){ err(error); }
|
|
||||||
else{ done(stream); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
ls: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((c) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
c.list(path, function(error, list) {
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{
|
|
||||||
list = list
|
|
||||||
.map(el => {
|
|
||||||
return {
|
|
||||||
size: el.size,
|
|
||||||
time: new Date(el.date).getTime(),
|
|
||||||
name: el.name,
|
|
||||||
type: function(t){
|
|
||||||
if(t === '-'){
|
|
||||||
return 'file';
|
|
||||||
}else if(t === 'd'){
|
|
||||||
return 'directory';
|
|
||||||
}else if(t === 'l'){
|
|
||||||
return 'link';
|
|
||||||
}
|
|
||||||
}(el.type),
|
|
||||||
can_read: null,
|
|
||||||
can_write: null,
|
|
||||||
can_delete: null,
|
|
||||||
can_move: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(el => {
|
|
||||||
return el.name === '.' || el.name === '..' ? false : true
|
|
||||||
});
|
|
||||||
done(list);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
write: function(path, content, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((c) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
c.put(content, path, function(error){
|
|
||||||
if (error){ err(error)}
|
|
||||||
else{ done('ok'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
rm: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((c) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
c.delete(path, function(error){
|
|
||||||
if(error){
|
|
||||||
c.rmdir(path, true, function(error){
|
|
||||||
if(error) { err(error) }
|
|
||||||
else{ done('ok dir'); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else{ done('ok'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mv: function(from, to, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((c) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
c.rename(from, to, function(error){
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{ done('ok') }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mkdir: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((c) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
c.mkdir(path, function(error){
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{ done('ok') }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
touch: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((c) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
c.put(Buffer.from(''), path, function(error){
|
|
||||||
if (error){ err(error)}
|
|
||||||
else{ done('ok'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
327
server/model/backend/gdrive.go
Normal file
327
server/model/backend/gdrive.go
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
|
"google.golang.org/api/drive/v3"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const gdriveFolderMarker = "application/vnd.google-apps.folder"
|
||||||
|
|
||||||
|
type GDrive struct {
|
||||||
|
Client *drive.Service
|
||||||
|
Config *oauth2.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGDrive(params map[string]string, app *App) (IBackend, error) {
|
||||||
|
backend := GDrive{}
|
||||||
|
if app.Config.OAuthProvider.GoogleDrive.ClientID == "" {
|
||||||
|
return backend, NewError("Missing Client ID: Contact your admin", 502)
|
||||||
|
} else if app.Config.OAuthProvider.GoogleDrive.ClientSecret == "" {
|
||||||
|
return backend, NewError("Missing Client Secret: Contact your admin", 502)
|
||||||
|
} else if app.Config.General.Host == "" {
|
||||||
|
return backend, NewError("Missing Hostname: Contact your admin", 502)
|
||||||
|
}
|
||||||
|
config := &oauth2.Config{
|
||||||
|
Endpoint: google.Endpoint,
|
||||||
|
ClientID: app.Config.OAuthProvider.GoogleDrive.ClientID,
|
||||||
|
ClientSecret: app.Config.OAuthProvider.GoogleDrive.ClientSecret,
|
||||||
|
RedirectURL: app.Config.General.Host + "/login",
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/drive"},
|
||||||
|
}
|
||||||
|
token := &oauth2.Token{
|
||||||
|
AccessToken: params["token"],
|
||||||
|
RefreshToken: params["refresh"],
|
||||||
|
Expiry: func(t string) time.Time {
|
||||||
|
expiry, err := strconv.ParseInt(t, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
return time.Unix(expiry, 0)
|
||||||
|
}(params["expiry"]),
|
||||||
|
TokenType: "bearer",
|
||||||
|
}
|
||||||
|
client := config.Client(context.Background(), token)
|
||||||
|
srv, err := drive.New(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError(err.Error(), 400)
|
||||||
|
}
|
||||||
|
backend.Client = srv
|
||||||
|
backend.Config = config
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) Info() string {
|
||||||
|
return "googledrive"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) OAuthURL() string {
|
||||||
|
return g.Config.AuthCodeURL("googledrive", oauth2.AccessTypeOnline)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) OAuthToken(ctx *map[string]string) error {
|
||||||
|
token, err := g.Config.Exchange(oauth2.NoContext, (*ctx)["code"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
(*ctx)["token"] = token.AccessToken
|
||||||
|
(*ctx)["refresh"] = token.RefreshToken
|
||||||
|
(*ctx)["expiry"] = strconv.FormatInt(token.Expiry.UnixNano()/1000, 10)
|
||||||
|
delete(*ctx, "code")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) Ls(path string) ([]os.FileInfo, error) {
|
||||||
|
files := make([]os.FileInfo, 0)
|
||||||
|
file, err := g.infoPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := g.Client.Files.List().Q("'" + file.id + "' in parents AND trashed = false").Fields("nextPageToken, files(name, size, modifiedTime, mimeType)").PageSize(500).Do()
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError(err.Error(), 404)
|
||||||
|
}
|
||||||
|
for _, obj := range res.Files {
|
||||||
|
files = append(files, File{
|
||||||
|
FName: obj.Name,
|
||||||
|
FType: func(mType string) string {
|
||||||
|
if mType == gdriveFolderMarker {
|
||||||
|
return "directory"
|
||||||
|
}
|
||||||
|
return "file"
|
||||||
|
}(obj.MimeType),
|
||||||
|
FTime: func(t string) int64 {
|
||||||
|
a, err := time.Parse(time.RFC3339, t)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return a.UnixNano() / 1000
|
||||||
|
}(obj.ModifiedTime),
|
||||||
|
FSize: obj.Size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) Cat(path string) (io.Reader, error) {
|
||||||
|
file, err := g.infoPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(file.mType, "application/vnd.google-apps") {
|
||||||
|
mType := "text/plain"
|
||||||
|
if file.mType == "application/vnd.google-apps.spreadsheet" {
|
||||||
|
mType = "text/csv"
|
||||||
|
}
|
||||||
|
data, err := g.Client.Files.Export(file.id, mType).Download()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := g.Client.Files.Get(file.id).Download()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) Mkdir(path string) error {
|
||||||
|
parent, err := g.infoPath(getParentPath(path))
|
||||||
|
if err != nil {
|
||||||
|
return NewError("Directory already exists", 409)
|
||||||
|
}
|
||||||
|
_, err = g.Client.Files.Create(&drive.File{
|
||||||
|
Name: filepath.Base(path),
|
||||||
|
Parents: []string{parent.id},
|
||||||
|
MimeType: gdriveFolderMarker,
|
||||||
|
}).Do()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) Rm(path string) error {
|
||||||
|
file, err := g.infoPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = g.Client.Files.Delete(file.id).Do(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) Mv(from string, to string) error {
|
||||||
|
ffile, err := g.infoPath(from)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tfile, err := g.infoPath(getParentPath(to))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = g.Client.Files.Update(ffile.id, &drive.File{
|
||||||
|
Name: filepath.Base(to),
|
||||||
|
}).RemoveParents(ffile.parent).AddParents(tfile.id).Do()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) Touch(path string) error {
|
||||||
|
file, err := g.infoPath(getParentPath(path))
|
||||||
|
if err != nil {
|
||||||
|
return NewError("Base folder not found", 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = g.Client.Files.Create(&drive.File{
|
||||||
|
Name: filepath.Base(path),
|
||||||
|
Parents: []string{file.id},
|
||||||
|
}).Media(strings.NewReader("")).Do()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) Save(path string, reader io.Reader) error {
|
||||||
|
if file, err := g.infoPath(path); err == nil {
|
||||||
|
_, err = g.Client.Files.Update(file.id, &drive.File{}).Media(reader).Do()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := g.infoPath(getParentPath(path))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = g.Client.Files.Create(&drive.File{
|
||||||
|
Name: filepath.Base(path),
|
||||||
|
Parents: []string{file.id},
|
||||||
|
}).Media(reader).Do()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GDrive) infoPath(p string) (*GDriveMarker, error) {
|
||||||
|
FindSolutions := func(level int, folder string) ([]GDriveMarker, error) {
|
||||||
|
res, err := g.Client.Files.List().Q("name = '" + folder + "' AND trashed = false").Fields("files(parents, id, name, mimeType)").PageSize(500).Do()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
solutions := make([]GDriveMarker, 0)
|
||||||
|
for _, file := range res.Files {
|
||||||
|
if len(file.Parents) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
solutions = append(solutions, GDriveMarker{
|
||||||
|
file.Id,
|
||||||
|
file.Parents[0],
|
||||||
|
file.Name,
|
||||||
|
level,
|
||||||
|
file.MimeType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return solutions, nil
|
||||||
|
}
|
||||||
|
FindRoot := func(level int) ([]GDriveMarker, error) {
|
||||||
|
root := make([]GDriveMarker, 0)
|
||||||
|
res, err := g.Client.Files.List().Q("'root' in parents").Fields("files(parents, id, name, mimeType)").PageSize(1).Do()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.Files) == 0 || len(res.Files[0].Parents) == 0 {
|
||||||
|
root = append(root, GDriveMarker{
|
||||||
|
"root",
|
||||||
|
"root",
|
||||||
|
"root",
|
||||||
|
level,
|
||||||
|
gdriveFolderMarker,
|
||||||
|
})
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
root = append(root, GDriveMarker{
|
||||||
|
res.Files[0].Parents[0],
|
||||||
|
"root",
|
||||||
|
"root",
|
||||||
|
level,
|
||||||
|
gdriveFolderMarker,
|
||||||
|
})
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
MergeSolutions := func(solutions_bag []GDriveMarker, solutions_new []GDriveMarker) []GDriveMarker {
|
||||||
|
if len(solutions_bag) == 0 {
|
||||||
|
return solutions_new
|
||||||
|
}
|
||||||
|
|
||||||
|
solutions := make([]GDriveMarker, 0)
|
||||||
|
for _, new := range solutions_new {
|
||||||
|
for _, old := range solutions_bag {
|
||||||
|
if new.id == old.parent && new.level+1 == old.level {
|
||||||
|
old.level = new.level
|
||||||
|
old.parent = new.id
|
||||||
|
solutions = append(solutions, old)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return solutions
|
||||||
|
}
|
||||||
|
var FindId func(folders []string, solutions_bag []GDriveMarker) (*GDriveMarker, error)
|
||||||
|
FindId = func(folders []string, solutions_bag []GDriveMarker) (*GDriveMarker, error) {
|
||||||
|
var solutions_new []GDriveMarker
|
||||||
|
var err error
|
||||||
|
if len(folders) == 0 {
|
||||||
|
solutions_new, err = FindRoot(0)
|
||||||
|
} else {
|
||||||
|
solutions_new, err = FindSolutions(len(folders), folders[len(folders)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError("Can't get data", 500)
|
||||||
|
}
|
||||||
|
solutions_bag = MergeSolutions(solutions_bag, solutions_new)
|
||||||
|
if len(solutions_bag) == 0 {
|
||||||
|
return nil, NewError("Doesn't exist", 404)
|
||||||
|
} else if len(solutions_bag) == 1 {
|
||||||
|
return &solutions_bag[0], nil
|
||||||
|
} else {
|
||||||
|
return FindId(folders[:len(folders)-1], solutions_bag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := make([]string, 0)
|
||||||
|
for _, chunk := range strings.Split(p, "/") {
|
||||||
|
if chunk == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path = append(path, chunk)
|
||||||
|
}
|
||||||
|
if len(path) == 0 {
|
||||||
|
return &GDriveMarker{
|
||||||
|
"root",
|
||||||
|
"",
|
||||||
|
"root",
|
||||||
|
0,
|
||||||
|
gdriveFolderMarker,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return FindId(path, make([]GDriveMarker, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
type GDriveMarker struct {
|
||||||
|
id string
|
||||||
|
parent string
|
||||||
|
name string
|
||||||
|
level int
|
||||||
|
mType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getParentPath(path string) string {
|
||||||
|
re := regexp.MustCompile("/$")
|
||||||
|
path = re.ReplaceAllString(path, "")
|
||||||
|
return filepath.Dir(path) + "/"
|
||||||
|
}
|
||||||
|
|
@ -1,500 +0,0 @@
|
||||||
// https://developers.google.com/drive/v3/web/quickstart/nodejs
|
|
||||||
// https://developers.google.com/apis-explorer/?hl=en_GB#p/drive/v3/
|
|
||||||
var google = require('googleapis'),
|
|
||||||
googleAuth = require('google-auth-library'),
|
|
||||||
config = require('../../../config_server'),
|
|
||||||
Stream = require('stream');
|
|
||||||
|
|
||||||
var client = google.drive('v3');
|
|
||||||
|
|
||||||
|
|
||||||
function findMimeType(filename){
|
|
||||||
let ext = filename.split('.').slice(-1)[0];
|
|
||||||
let list = {
|
|
||||||
xls: 'application/vnd.ms-excel',
|
|
||||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
||||||
xml: 'text/xml',
|
|
||||||
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
||||||
csv: 'text/csv',
|
|
||||||
tmpl: 'text/plain',
|
|
||||||
org: 'text/plain',
|
|
||||||
md: 'text/plain',
|
|
||||||
pdf: 'application/pdf',
|
|
||||||
php: 'application/x-httpd-php',
|
|
||||||
jpg: 'image/jpeg',
|
|
||||||
png: 'image/png',
|
|
||||||
gif: 'image/gif',
|
|
||||||
bmp: 'image/bmp',
|
|
||||||
txt: 'text/plain',
|
|
||||||
text: 'text/plain',
|
|
||||||
conf: 'text/plain',
|
|
||||||
log: 'text/plain',
|
|
||||||
doc: 'application/msword',
|
|
||||||
js: 'text/js',
|
|
||||||
swf: 'application/x-shockwave-flash',
|
|
||||||
mp3: 'audio/mpeg',
|
|
||||||
zip: 'application/zip',
|
|
||||||
rar: 'application/rar',
|
|
||||||
tar: 'application/tar',
|
|
||||||
arj: 'application/arj',
|
|
||||||
cab: 'application/cab',
|
|
||||||
html: 'text/html',
|
|
||||||
htm: 'text/html'
|
|
||||||
};
|
|
||||||
return list[ext] || 'application/octet-stream';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function decode(path){
|
|
||||||
let tmp = path.trim().split('/');
|
|
||||||
let filename = tmp.pop() || null;
|
|
||||||
tmp.shift();
|
|
||||||
return {
|
|
||||||
name: filename,
|
|
||||||
parents: tmp,
|
|
||||||
full: filename === null ? tmp : [].concat(tmp, [filename])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function findId(auth, _folders, ids = []){
|
|
||||||
const folders = JSON.parse(JSON.stringify(_folders));
|
|
||||||
const name = folders.pop();
|
|
||||||
|
|
||||||
return search(auth, name, folders)
|
|
||||||
.then((files) => {
|
|
||||||
let solutions = findSolutions(files, ids);
|
|
||||||
let aggregatedSolution = [].concat(solutions, ids);
|
|
||||||
if(solutions.length === 0){
|
|
||||||
return Promise.reject({message: 'this path doesn\'t exist', code: 'UNKNOWN_PATH'});
|
|
||||||
}else if(solutions.length === 1){
|
|
||||||
return Promise.resolve(findFolderId(solutions[0], ids));
|
|
||||||
}else{
|
|
||||||
return findId(auth, folders, aggregatedSolution);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function search(_auth, _name, _folders){
|
|
||||||
if(_name === undefined){
|
|
||||||
return findRoot(_auth);
|
|
||||||
}else{
|
|
||||||
return findByName(_auth, _name, _folders.length + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findRoot(auth){
|
|
||||||
return new Promise((_done,_err) => {
|
|
||||||
client.files.list({
|
|
||||||
auth: auth,
|
|
||||||
q: "'root' in parents",
|
|
||||||
pageSize: 1,
|
|
||||||
fields: "files(parents, id, name)"
|
|
||||||
}, function(error, response){
|
|
||||||
if(error){_err(error);}
|
|
||||||
else{
|
|
||||||
if(response.files.length > 0){
|
|
||||||
_done(response.files.map((file) => {
|
|
||||||
return {
|
|
||||||
level: 0,
|
|
||||||
id: file.parents[0],
|
|
||||||
name: 'root'
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
}else{
|
|
||||||
_done([{
|
|
||||||
level: 0,
|
|
||||||
id: 'root',
|
|
||||||
name: 'root'
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function findByName(auth, name, _level){
|
|
||||||
return new Promise((_done,_err) => {
|
|
||||||
client.files.list({
|
|
||||||
auth: auth,
|
|
||||||
q: "name = '"+name+"' AND trashed = false",
|
|
||||||
pageSize: 500,
|
|
||||||
fields: "files(parents, id, name)"
|
|
||||||
}, function(error, response){
|
|
||||||
if(error){_err(error);}
|
|
||||||
else{
|
|
||||||
_done(response.files.map((file) => {
|
|
||||||
file.level = _level;
|
|
||||||
return file;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function findFolderId(head, cache, result = 'root'){
|
|
||||||
for(let i=0, l=cache.length; i<l; i++){
|
|
||||||
if(head.id === cache[i].parents[0] && head.level + 1 === cache[i].level){
|
|
||||||
return findFolderId(cache[i], cache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return head.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findSolutions(newFiles, cache){
|
|
||||||
return newFiles.filter((newFile) => {
|
|
||||||
if(cache.length === 0){ return true;}
|
|
||||||
for(let i=0, j=cache.length; i<j; i++){
|
|
||||||
if(newFile.id === cache[i].parents[0] && (newFile.level + 1) === cache[i].level){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function authorize(params){
|
|
||||||
var auth = new googleAuth(),
|
|
||||||
client_id = config.gdrive.clientID,
|
|
||||||
client_secret = config.gdrive.clientSecret,
|
|
||||||
redirect_uri = config.gdrive.redirectURI;
|
|
||||||
|
|
||||||
var oauth2Client = new auth.OAuth2(client_id, client_secret, redirect_uri);
|
|
||||||
return Promise.resolve(oauth2Client);
|
|
||||||
}
|
|
||||||
function connect(params){
|
|
||||||
return authorize(params)
|
|
||||||
.then((auth) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
if(params && params.access_token){
|
|
||||||
auth.credentials = params;
|
|
||||||
done(auth);
|
|
||||||
}else if(params && params.code){
|
|
||||||
auth.getToken(params.code, function(error, token) {
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{
|
|
||||||
auth.credentials = token;
|
|
||||||
done(auth);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
err({message: 'can\'t connect without auth code or token', code: 'INVALID_CONNECTION'});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve(auth);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
auth: function(params){
|
|
||||||
return authorize()
|
|
||||||
.then((auth) => {
|
|
||||||
return Promise.resolve(auth.generateAuthUrl({
|
|
||||||
access_type: 'online',
|
|
||||||
scope: [ "https://www.googleapis.com/auth/drive" ]
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
test: function(params){
|
|
||||||
return connect(params)
|
|
||||||
.then((auth) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
client.files.list({
|
|
||||||
auth: auth,
|
|
||||||
q: "'root' in parents AND mimeType = 'application/vnd.google-apps.folder'",
|
|
||||||
pageSize: 5,
|
|
||||||
fields: "files(parents)"
|
|
||||||
}, function(error, response) {
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done(auth.credentials); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
cat: function(path, params){
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((auth) => {
|
|
||||||
return findId(auth, path.full)
|
|
||||||
.then((id) => fileInfo(auth, id))
|
|
||||||
.then((file) => {
|
|
||||||
if(/application\/vnd.google-apps/.test(file.mimeType)){
|
|
||||||
let type = 'text/plain';
|
|
||||||
if(file.mimeType === 'application/vnd.google-apps.spreadsheet'){
|
|
||||||
type = 'text/csv';
|
|
||||||
}
|
|
||||||
return exporter(auth, file.id, type);
|
|
||||||
}else{
|
|
||||||
return download(auth, file.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(function(stream){
|
|
||||||
stream.on('response', function(response) {
|
|
||||||
delete response.headers;
|
|
||||||
});
|
|
||||||
return Promise.resolve(stream);
|
|
||||||
});
|
|
||||||
|
|
||||||
function fileInfo(auth, id){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
client.files.get({
|
|
||||||
auth: auth,
|
|
||||||
fileId: id
|
|
||||||
},function(error, response){
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done(response); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function download(auth, id){
|
|
||||||
var content = '';
|
|
||||||
return Promise.resolve(client.files.get({
|
|
||||||
auth: auth,
|
|
||||||
fileId: id,
|
|
||||||
alt: 'media'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
function exporter(auth, id, type){
|
|
||||||
var content = '';
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
done(client.files.export({
|
|
||||||
auth: auth,
|
|
||||||
fileId: id,
|
|
||||||
mimeType: type
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ls: function(_path, params){
|
|
||||||
path = decode(_path);
|
|
||||||
return connect(params)
|
|
||||||
.then((auth) => {
|
|
||||||
return findId(auth, path.parents)
|
|
||||||
.then((id) => findDrive(auth, id))
|
|
||||||
.then(parse);
|
|
||||||
});
|
|
||||||
|
|
||||||
function findDrive(auth, id){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
client.files.list({
|
|
||||||
spaces: path.space,
|
|
||||||
auth: auth,
|
|
||||||
q: "'"+id+"' in parents AND trashed = false",
|
|
||||||
pageSize: 500,
|
|
||||||
fields: "files(id,mimeType,modifiedTime,name,size)"
|
|
||||||
}, function(error, response) {
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done(response.files); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function parse(files){
|
|
||||||
return Promise.resolve(files.map((file) => {
|
|
||||||
return {
|
|
||||||
type: file.mimeType === 'application/vnd.google-apps.folder'? 'directory' : 'file',
|
|
||||||
name: file.name,
|
|
||||||
size: file.hasOwnProperty('size')? Number(file.size) : 0,
|
|
||||||
time: new Date(file.modifiedTime).getTime()
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
write: function(path, content, params){ // TODO
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((auth) => {
|
|
||||||
return fileAlreadyExist(auth, path)
|
|
||||||
.then((obj) => {
|
|
||||||
if(obj.alreadyExist === true){
|
|
||||||
return updateFile(auth, content, path.name, obj.id);
|
|
||||||
}
|
|
||||||
if(obj.alreadyExist === false){
|
|
||||||
return createFile(auth, content, path.name, obj.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function fileAlreadyExist(auth, path){
|
|
||||||
return findId(auth, path.full)
|
|
||||||
.then((id) => Promise.resolve({alreadyExist: true, id: id}))
|
|
||||||
.catch((err) => {
|
|
||||||
return findId(auth, path.parents)
|
|
||||||
.then((id) => Promise.resolve({alreadyExist: false, id: id}))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFile(_auth, _stream, _filename, _folderId){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
client.files.create({
|
|
||||||
auth: _auth,
|
|
||||||
fields: 'id',
|
|
||||||
media: {
|
|
||||||
mimeType: 'text/plain',
|
|
||||||
body: _stream
|
|
||||||
},
|
|
||||||
resource: {
|
|
||||||
name: _filename,
|
|
||||||
parents: [_folderId]
|
|
||||||
}
|
|
||||||
}, function(error){
|
|
||||||
if(error) {err(error); }
|
|
||||||
else{ done('ok'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function updateFile(_auth, _stream, _filename, _folderId){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
client.files.update({
|
|
||||||
auth: _auth,
|
|
||||||
fileId: _folderId,
|
|
||||||
fields: 'id',
|
|
||||||
media: {
|
|
||||||
mimeType: findMimeType(_filename),
|
|
||||||
body: _stream
|
|
||||||
}
|
|
||||||
}, function(error){
|
|
||||||
if(error) {err(error); }
|
|
||||||
else{ done('ok'); }
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
rm: function(path, params){
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((auth) => {
|
|
||||||
return findId(auth, path.full)
|
|
||||||
.then((id) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
client.files.delete({
|
|
||||||
fileId: id,
|
|
||||||
auth: auth
|
|
||||||
}, function(error){
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done('ok'); }
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mv: function(from, to, params){
|
|
||||||
from = decode(from);
|
|
||||||
to = decode(to);
|
|
||||||
return connect(params)
|
|
||||||
.then((auth) => {
|
|
||||||
return Promise.all([findId(auth, from.full), findId(auth, from.parents), findId(auth, to.parents)])
|
|
||||||
.then((res) => process(auth, res));
|
|
||||||
});
|
|
||||||
|
|
||||||
function wait(res){
|
|
||||||
return new Promise((done) => {
|
|
||||||
setTimeout(function(){
|
|
||||||
done(res);
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function process(auth, res){
|
|
||||||
let fileId = res[0],
|
|
||||||
srcId = res[1],
|
|
||||||
destId = res[2];
|
|
||||||
let fields = 'id';
|
|
||||||
let params = {fileId, fileId, auth: auth};
|
|
||||||
|
|
||||||
if(destId !== srcId){
|
|
||||||
fields += ', parents';
|
|
||||||
params.addParents = destId;
|
|
||||||
params.removeParents = srcId;
|
|
||||||
}
|
|
||||||
if(to.name !== null && from.name !== null && from.name !== to.name ){
|
|
||||||
fields += 'name';
|
|
||||||
params.resource = {
|
|
||||||
name: to.name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
client.files.update(params, function(error, response){
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done('ok'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mkdir: function(path, params){
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((auth) => {
|
|
||||||
return findId(auth, path.parents.slice(0, -1))
|
|
||||||
.then((folder) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
client.files.create({
|
|
||||||
fields: 'id',
|
|
||||||
auth: auth,
|
|
||||||
resource: {
|
|
||||||
name: path.parents.slice(-1)[0],
|
|
||||||
parents: [folder],
|
|
||||||
mimeType: 'application/vnd.google-apps.folder'
|
|
||||||
}
|
|
||||||
}, function(error){
|
|
||||||
if(error) {err(error); }
|
|
||||||
else{ done(auth); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((auth) => verifyFolderCreation(auth, path.full));
|
|
||||||
|
|
||||||
function verifyFolderCreation(_auth, _path, n = 10){
|
|
||||||
return sleep(Math.abs(10 - n) * 100)
|
|
||||||
.then(() => findId(_auth, _path))
|
|
||||||
.catch((err) => {
|
|
||||||
if(n > 0 && err && err.code === 'UNKNOWN_PATH'){
|
|
||||||
return verifyFolderCreation(_auth, _path, n - 1);
|
|
||||||
}
|
|
||||||
return Promise.reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
function sleep(t=1000, arg){
|
|
||||||
return new Promise((done) => {
|
|
||||||
setTimeout(function(){
|
|
||||||
done(arg);
|
|
||||||
}, t);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function copy(obj){
|
|
||||||
return JSON.parse(JSON.stringify(obj));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
touch: function(path, params){
|
|
||||||
path = decode(path);
|
|
||||||
var readable = new Stream.Readable();
|
|
||||||
readable.push('');
|
|
||||||
readable.push(null);
|
|
||||||
|
|
||||||
return connect(params)
|
|
||||||
.then((auth) => {
|
|
||||||
return findId(auth, path.parents)
|
|
||||||
.then((folder) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
client.files.create({
|
|
||||||
auth: auth,
|
|
||||||
fields: 'id',
|
|
||||||
media: {
|
|
||||||
mimeType: 'text/plain',
|
|
||||||
body: readable
|
|
||||||
},
|
|
||||||
resource: {
|
|
||||||
name: path.name,
|
|
||||||
parents: [folder]
|
|
||||||
}
|
|
||||||
}, function(error){
|
|
||||||
if(error) {err(error); }
|
|
||||||
else{ done('ok'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
347
server/model/backend/git.go
Normal file
347
server/model/backend/git.go
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"github.com/mitchellh/hashstructure"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"gopkg.in/src-d/go-git.v4"
|
||||||
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||||
|
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||||
|
"gopkg.in/src-d/go-git.v4/plumbing/transport"
|
||||||
|
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
|
||||||
|
sshgit "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const GitCachePath = "data/cache/git/"
|
||||||
|
|
||||||
|
var GitCache AppCache
|
||||||
|
|
||||||
|
type Git struct {
|
||||||
|
git *GitLib
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
GitCache = NewAppCache()
|
||||||
|
cachePath := filepath.Join(GetCurrentDir(), GitCachePath)
|
||||||
|
os.RemoveAll(cachePath)
|
||||||
|
os.MkdirAll(cachePath, os.ModePerm)
|
||||||
|
GitCache.OnEvict(func(key string, value interface{}) {
|
||||||
|
g := value.(*Git)
|
||||||
|
g.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitParams struct {
|
||||||
|
repo string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
passphrase string
|
||||||
|
commit string
|
||||||
|
branch string
|
||||||
|
authorName string
|
||||||
|
authorEmail string
|
||||||
|
committerName string
|
||||||
|
committerEmail string
|
||||||
|
basePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGit(params map[string]string, app *App) (*Git, error) {
|
||||||
|
if obj := GitCache.Get(params); obj != nil {
|
||||||
|
return obj.(*Git), nil
|
||||||
|
}
|
||||||
|
g := &Git{
|
||||||
|
git: &GitLib{
|
||||||
|
params: &GitParams{
|
||||||
|
params["repo"],
|
||||||
|
params["username"],
|
||||||
|
params["password"],
|
||||||
|
params["passphrase"],
|
||||||
|
params["commit"],
|
||||||
|
params["branch"],
|
||||||
|
params["authorName"],
|
||||||
|
params["authorEmail"],
|
||||||
|
params["committerName"],
|
||||||
|
params["committerEmail"],
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p := g.git.params
|
||||||
|
if p.branch == "" {
|
||||||
|
p.branch = "master"
|
||||||
|
}
|
||||||
|
if p.commit == "" {
|
||||||
|
p.commit = "{action} ({filename}): {path}"
|
||||||
|
}
|
||||||
|
if p.authorName == "" {
|
||||||
|
p.authorName = "Nuage"
|
||||||
|
}
|
||||||
|
if p.authorEmail == "" {
|
||||||
|
p.authorEmail = "https://nuage.kerjean.me"
|
||||||
|
}
|
||||||
|
if p.committerName == "" {
|
||||||
|
p.committerName = "Nuage"
|
||||||
|
}
|
||||||
|
if p.committerEmail == "" {
|
||||||
|
p.committerEmail = "https://nuage.kerjean.me"
|
||||||
|
}
|
||||||
|
if len(params["password"]) > 2700 {
|
||||||
|
return nil, NewError("Your password doesn't fit in a cookie :/", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := hashstructure.Hash(params, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError("Internal error", 500)
|
||||||
|
}
|
||||||
|
p.basePath = app.Helpers.AbsolutePath(GitCachePath + "repo_" + fmt.Sprint(hash) + "/")
|
||||||
|
|
||||||
|
repo, err := g.git.open(p, p.basePath)
|
||||||
|
g.git.repo = repo
|
||||||
|
if err != nil {
|
||||||
|
return g, err
|
||||||
|
}
|
||||||
|
GitCache.Set(params, g)
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) Info() string {
|
||||||
|
return "git"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) Ls(path string) ([]os.FileInfo, error) {
|
||||||
|
g.git.refresh()
|
||||||
|
p, err := g.path(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
file, err := os.Open(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return file.Readdir(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) Cat(path string) (io.Reader, error) {
|
||||||
|
p, err := g.path(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
return os.Open(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) Mkdir(path string) error {
|
||||||
|
p, err := g.path(path)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
return os.Mkdir(p, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) Rm(path string) error {
|
||||||
|
p, err := g.path(path)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
if err = os.RemoveAll(p); err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
message := g.git.message("delete", path)
|
||||||
|
if err = g.git.save(message); err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) Mv(from string, to string) error {
|
||||||
|
fpath, err := g.path(from)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
tpath, err := g.path(to)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Rename(fpath, tpath); err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
message := g.git.message("move", from)
|
||||||
|
if err = g.git.save(message); err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) Touch(path string) error {
|
||||||
|
p, err := g.path(path)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
file, err := os.Create(p)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
message := g.git.message("create", path)
|
||||||
|
if err = g.git.save(message); err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) Save(path string, file io.Reader) error {
|
||||||
|
p, err := g.path(path)
|
||||||
|
if err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
fo, err := os.Create(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
io.Copy(fo, file)
|
||||||
|
fo.Close()
|
||||||
|
|
||||||
|
message := g.git.message("save", path)
|
||||||
|
if err = g.git.save(message); err != nil {
|
||||||
|
return NewError(err.Error(), 403)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) Close() error {
|
||||||
|
return os.RemoveAll(g.git.params.basePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Git) path(path string) (string, error) {
|
||||||
|
if path == "" {
|
||||||
|
return "", NewError("No path available", 400)
|
||||||
|
}
|
||||||
|
basePath := filepath.Join(g.git.params.basePath, path)
|
||||||
|
if string(path[len(path)-1]) == "/" {
|
||||||
|
basePath += "/"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(basePath, g.git.params.basePath) == false {
|
||||||
|
return "", NewError("There's nothing here", 403)
|
||||||
|
}
|
||||||
|
return basePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitLib struct {
|
||||||
|
repo *git.Repository
|
||||||
|
params *GitParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GitLib) open(params *GitParams, path string) (*git.Repository, error) {
|
||||||
|
g.params = params
|
||||||
|
|
||||||
|
if _, err := os.Stat(g.params.basePath); os.IsNotExist(err) {
|
||||||
|
auth, err := g.auth()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return git.PlainClone(path, false, &git.CloneOptions{
|
||||||
|
URL: g.params.repo,
|
||||||
|
Depth: 1,
|
||||||
|
ReferenceName: plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", g.params.branch)),
|
||||||
|
SingleBranch: true,
|
||||||
|
Auth: auth,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return git.PlainOpen(g.params.basePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GitLib) save(message string) error {
|
||||||
|
w, err := g.repo.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
return NewError(err.Error(), 500)
|
||||||
|
}
|
||||||
|
_, err = w.Add(".")
|
||||||
|
if err != nil {
|
||||||
|
return NewError(err.Error(), 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.Commit(message, &git.CommitOptions{
|
||||||
|
All: true,
|
||||||
|
Author: &object.Signature{
|
||||||
|
Name: g.params.authorName,
|
||||||
|
Email: g.params.authorEmail,
|
||||||
|
When: time.Now(),
|
||||||
|
},
|
||||||
|
Committer: &object.Signature{
|
||||||
|
Name: g.params.committerName,
|
||||||
|
Email: g.params.committerEmail,
|
||||||
|
When: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := g.auth()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return g.repo.Push(&git.PushOptions{
|
||||||
|
Auth: auth,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GitLib) refresh() error {
|
||||||
|
w, err := g.repo.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return w.Pull(&git.PullOptions{RemoteName: "origin"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GitLib) auth() (transport.AuthMethod, error) {
|
||||||
|
if strings.HasPrefix(g.params.repo, "http") {
|
||||||
|
return &http.BasicAuth{
|
||||||
|
Username: g.params.username,
|
||||||
|
Password: g.params.password,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
isPrivateKey := func(pass string) bool {
|
||||||
|
if len(pass) > 1000 && strings.HasPrefix(pass, "-----") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPrivateKey(g.params.password) {
|
||||||
|
signer, err := ssh.ParsePrivateKeyWithPassphrase([]byte(g.params.password), []byte(g.params.passphrase))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sshgit.PublicKeys{
|
||||||
|
User: "git",
|
||||||
|
Signer: signer,
|
||||||
|
HostKeyCallbackHelper: sshgit.HostKeyCallbackHelper{
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sshgit.Password{
|
||||||
|
User: g.params.username,
|
||||||
|
Password: g.params.password,
|
||||||
|
HostKeyCallbackHelper: sshgit.HostKeyCallbackHelper{
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GitLib) message(action string, path string) string {
|
||||||
|
message := strings.Replace(g.params.commit, "{action}", "save", -1)
|
||||||
|
message = strings.Replace(message, "{filename}", filepath.Base(path), -1)
|
||||||
|
message = strings.Replace(message, "{path}", strings.Replace(path, g.params.basePath, "", -1), -1)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
@ -1,338 +0,0 @@
|
||||||
const gitclient = require("nodegit"),
|
|
||||||
toString = require('stream-to-string'),
|
|
||||||
fs = require('fs'),
|
|
||||||
Readable = require('stream').Readable,
|
|
||||||
Path = require('path'),
|
|
||||||
crypto = require("crypto"),
|
|
||||||
BASE_PATH = "/tmp/";
|
|
||||||
|
|
||||||
let repos = {};
|
|
||||||
setInterval(autoVacuum, 1000*60*60); // autovacuum every hour
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
test: function(params){
|
|
||||||
if(!params || !params.repo){ return Promise.reject({message: 'invalid authentication', code: 'INVALID_PARAMS'}) };
|
|
||||||
if(!params.commit) params.commit = "{action} ({filename}): {path}";
|
|
||||||
if(!params.branch) params.branch = 'master';
|
|
||||||
if(!params.author_name) params.author_name = "Nuage";
|
|
||||||
if(!params.author_email) params.author_email = "https://nuage.kerjean.me";
|
|
||||||
if(!params.committer_name) params.committer_name = "Nuage";
|
|
||||||
if(!params.committer_email) params.committer_email = "https://nuage.kerjean.me";
|
|
||||||
|
|
||||||
if(params.password && params.password.length > 2700){
|
|
||||||
return Promise.reject({message: "Your password couldn\'t fit in a cookie :/", code: "COOKIE_ERROR"})
|
|
||||||
}
|
|
||||||
return git.open(params)
|
|
||||||
.then(() => Promise.resolve(params));
|
|
||||||
},
|
|
||||||
cat: function(path, params){
|
|
||||||
return git.open(params)
|
|
||||||
.then((repo) => git.refresh(repo, params))
|
|
||||||
.then(() => file.cat(calculate_path(params, path)));
|
|
||||||
},
|
|
||||||
ls: function(path, params){
|
|
||||||
return git.open(params)
|
|
||||||
.then((repo) => git.refresh(repo, params))
|
|
||||||
.then(() => file.ls(calculate_path(params, path)))
|
|
||||||
.then((files) => files.filter((file) => (file.name === '.git' && file.type === 'directory') ? false: true));
|
|
||||||
},
|
|
||||||
write: function(path, content, params){
|
|
||||||
return git.open(params)
|
|
||||||
.then(() => file.write(calculate_path(params, path), content))
|
|
||||||
.then(() => git.save(params, path, "write"));
|
|
||||||
},
|
|
||||||
rm: function(path, params){
|
|
||||||
return git.open(params)
|
|
||||||
.then(() => file.rm(calculate_path(params, path)))
|
|
||||||
.then(() => git.save(params, path, "delete"));
|
|
||||||
},
|
|
||||||
mv: function(from, to, params){
|
|
||||||
return git.open(params)
|
|
||||||
.then(() => file.mv(calculate_path(params, from), calculate_path(params, to)))
|
|
||||||
.then(() => git.save(params, to, 'move'));
|
|
||||||
},
|
|
||||||
mkdir: function(path, params){
|
|
||||||
return git.open(params)
|
|
||||||
.then(() => file.mkdir(calculate_path(params, path)))
|
|
||||||
.then(() => git.save(params, path, "create"))
|
|
||||||
},
|
|
||||||
touch: function(path, params){
|
|
||||||
var stream = new Readable(); stream.push(''); stream.push(null);
|
|
||||||
return git.open(params)
|
|
||||||
.then(() => file.write(calculate_path(params, path), stream))
|
|
||||||
.then(() => git.save(params, path, 'create'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function autoVacuum(){
|
|
||||||
file.ls(BASE_PATH).then((files) => {
|
|
||||||
files.map((_file) => {
|
|
||||||
const filename = _file.name,
|
|
||||||
full_path = BASE_PATH + filename;
|
|
||||||
|
|
||||||
if(wasCreatedByTheGitBackend(full_path) === false) return;
|
|
||||||
|
|
||||||
if(repos[full_path] === undefined){
|
|
||||||
// remove stuff that was created in a previous session
|
|
||||||
// => happen on server restart
|
|
||||||
remove(full_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up after 5 hours without activity in the repo
|
|
||||||
const MAXIMUM_DATE_BEFORE_CLEAN = repos[full_path] + 1000*60*60*5;
|
|
||||||
if(new Date().getTime() > MAXIMUM_DATE_BEFORE_CLEAN){
|
|
||||||
remove(full_path);
|
|
||||||
delete repos[full_path];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function remove(path){
|
|
||||||
return file.rm(path).catch((err) => {
|
|
||||||
console.warn("WARNING: vacuum", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function wasCreatedByTheGitBackend(name){
|
|
||||||
return name.indexOf(BASE_PATH+"git_") === 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculate_path(params, path){
|
|
||||||
const repo = path_repo(params);
|
|
||||||
const full_path = Path.posix.join(repo, path);
|
|
||||||
if(full_path.indexOf(BASE_PATH) !== 0 || full_path === BASE_PATH){
|
|
||||||
return BASE_PATH+"error";
|
|
||||||
}
|
|
||||||
return full_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
function path_repo(obj){
|
|
||||||
let hash = crypto.createHash('md5');
|
|
||||||
for(let key in obj){
|
|
||||||
if(typeof obj[key] === 'string'){
|
|
||||||
hash.update(obj[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const path = BASE_PATH+"git_"+obj.uid+"_"+obj.repo.replace(/[^a-zA-Z]/g, "")+"_"+hash.digest('hex');
|
|
||||||
repos[path] = new Date().getTime();
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = {};
|
|
||||||
file.write = function (path, stream){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
let writer = fs.createWriteStream(path, { flags : 'w' });
|
|
||||||
stream.pipe(writer);
|
|
||||||
writer.on('close', function(){
|
|
||||||
done('ok');
|
|
||||||
});
|
|
||||||
writer.on('error', function(error){
|
|
||||||
err(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
file.mkdir = function(path){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
fs.mkdir(path, function(error){
|
|
||||||
if(error){ return err(error); }
|
|
||||||
return done("ok");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
file.mv = function(from, to){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
fs.rename(from, to, function(error){
|
|
||||||
if(error){ return err(error); }
|
|
||||||
return done("ok");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
file.ls = function(path){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
fs.readdir(path, (error, files) => {
|
|
||||||
if(error){ return err(error); }
|
|
||||||
Promise.all(files.map((file) => {
|
|
||||||
return stats(path+file).then((stat) => {
|
|
||||||
stat.name = file;
|
|
||||||
return Promise.resolve(stat);
|
|
||||||
});
|
|
||||||
})).then((files) => {
|
|
||||||
done(files.map((file) => {
|
|
||||||
return {
|
|
||||||
size: file.size,
|
|
||||||
time: new Date(file.mtime).getTime(),
|
|
||||||
name: file.name,
|
|
||||||
type: file.isFile()? 'file' : 'directory'
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
}).catch((error) => err(error));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function stats(path){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
fs.stat(path, function(error, res){
|
|
||||||
if(error) return err(error);
|
|
||||||
return done(res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file.rm = function(path){
|
|
||||||
return rm(path);
|
|
||||||
|
|
||||||
function rm(path){
|
|
||||||
return stat(path).then((_stat) => {
|
|
||||||
if(_stat.isDirectory()){
|
|
||||||
return ls(path)
|
|
||||||
.then((files) => Promise.all(files.map(file => rm(path+"/"+file))))
|
|
||||||
.then(() => removeEmptyFolder(path));
|
|
||||||
}else{
|
|
||||||
return removeFileOrLink(path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeEmptyFolder(path){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
fs.rmdir(path, function(error){
|
|
||||||
if(error){ return err(error); }
|
|
||||||
return done("ok");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function removeFileOrLink(path){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
fs.unlink(path, function(error){
|
|
||||||
if(error){ return err(error); }
|
|
||||||
return done("ok");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function ls(path){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
fs.readdir(path, function (error, files) {
|
|
||||||
if(error) return err(error)
|
|
||||||
return done(files)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function stat(path){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
fs.stat(path, function (error, _stat) {
|
|
||||||
if(error){ return err(error); }
|
|
||||||
return done(_stat);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file.cat = function(path){
|
|
||||||
return Promise.resolve(fs.createReadStream(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const git = {};
|
|
||||||
git.open = function(params){
|
|
||||||
count = 0;
|
|
||||||
return gitclient.Repository.open(path_repo(params))
|
|
||||||
.catch((err) => {
|
|
||||||
return gitclient.Clone(params.repo, path_repo(params), {fetchOpts: { callbacks: { credentials: git_creds.bind(null, params) }}})
|
|
||||||
.then((repo) => {
|
|
||||||
const branch = params.branch;
|
|
||||||
return repo.getBranchCommit("origin/"+branch)
|
|
||||||
.catch(() => repo.getHeadCommit("origin"))
|
|
||||||
.then((commit) => {
|
|
||||||
return repo.createBranch(branch, commit)
|
|
||||||
.then(() => repo.checkoutBranch(branch))
|
|
||||||
.then(() => Promise.resolve(repo));
|
|
||||||
})
|
|
||||||
.catch(() => Promise.resolve(repo));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
git.refresh = function(repo, params){
|
|
||||||
count = 0;
|
|
||||||
return repo.fetchAll({callbacks: { credentials: git_creds.bind(null, params) }})
|
|
||||||
.then(() => repo.mergeBranches(params.branch, "origin/"+params.branch, gitclient.Signature.default(repo), 2))
|
|
||||||
.catch(err => {
|
|
||||||
if(err.errno === -13){
|
|
||||||
return git.save(params, '', 'merge')
|
|
||||||
.then(() => git.refresh(repo, params))
|
|
||||||
.then(() => Promise.resolve(repo));
|
|
||||||
}
|
|
||||||
return Promise.resolve(repo);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
git.save = function(params, path = '', type = ''){
|
|
||||||
count = 0;
|
|
||||||
const author = gitclient.Signature.now(params.author_name, params.author_email);
|
|
||||||
const committer = gitclient.Signature.now(params.committer_name, params.committer_email);
|
|
||||||
const message = params.commit
|
|
||||||
.replace("{action}", type)
|
|
||||||
.replace("{dirname}", Path.posix.dirname(path))
|
|
||||||
.replace("{filename}", Path.posix.basename(path))
|
|
||||||
.replace("{path}", path || '');
|
|
||||||
|
|
||||||
return git.open(params)
|
|
||||||
.then((repo) => Promise.all([
|
|
||||||
Promise.resolve(repo),
|
|
||||||
getParent(repo, params),
|
|
||||||
refresh(repo, params)
|
|
||||||
]))
|
|
||||||
.then((data) => {
|
|
||||||
const [repo, commit, oid] = data;
|
|
||||||
const parents = commit ? [commit] : [];
|
|
||||||
return repo.createCommit("HEAD", author, committer, message, oid, parents)
|
|
||||||
.then(() => Promise.resolve(repo));
|
|
||||||
})
|
|
||||||
.then((repo) => {
|
|
||||||
return repo.getRemote("origin")
|
|
||||||
.then((remote) => {
|
|
||||||
return remote.push(
|
|
||||||
["refs/heads/"+params.branch+":refs/heads/"+params.branch],
|
|
||||||
{ callbacks: { credentials: git_creds.bind(null, params, true) }}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((err) => Promise.reject({status: 403, message: "Not authorized to push"}));
|
|
||||||
});
|
|
||||||
|
|
||||||
function getParent(repo, params){
|
|
||||||
return repo.getBranchCommit(params.branch)
|
|
||||||
.catch(() => {
|
|
||||||
return repo.getHeadCommit()
|
|
||||||
.catch(() => Promise.resolve(null));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function refresh(repo, params){
|
|
||||||
return repo.refreshIndex()
|
|
||||||
.then((index) => {
|
|
||||||
return index.addAll()
|
|
||||||
.then(() => index.write())
|
|
||||||
.then(() => index.writeTree());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// the count thinghy is used to see if the request succeeded or not
|
|
||||||
// when something fail, nodegit would just run the callback again and again.
|
|
||||||
// The only way to make it throw an error is to return the defaultNew thinghy
|
|
||||||
let count = 0;
|
|
||||||
function git_creds(params, fn, _count){
|
|
||||||
count += 1;
|
|
||||||
|
|
||||||
if(count > 1 && _count !== undefined){
|
|
||||||
return new gitclient.Cred.defaultNew();
|
|
||||||
}else if(/http[s]?\:\/\//.test(params.repo)){
|
|
||||||
return new gitclient.Cred.userpassPlaintextNew(params.username, params.password);
|
|
||||||
}else{
|
|
||||||
return new gitclient.Cred.sshKeyMemoryNew(params.username, "", params.password, params.passphrase || "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
42
server/model/backend/nothing.go
Normal file
42
server/model/backend/nothing.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Nothing struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNothing(params map[string]string, app *App) (*Nothing, error) {
|
||||||
|
return &Nothing{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Nothing) Info() string {
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Nothing) Ls(path string) ([]os.FileInfo, error) {
|
||||||
|
return nil, NewError("", 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Nothing) Cat(path string) (io.Reader, error) {
|
||||||
|
return strings.NewReader(""), NewError("", 401)
|
||||||
|
}
|
||||||
|
func (b Nothing) Mkdir(path string) error {
|
||||||
|
return NewError("", 401)
|
||||||
|
}
|
||||||
|
func (b Nothing) Rm(path string) error {
|
||||||
|
return NewError("", 401)
|
||||||
|
}
|
||||||
|
func (b Nothing) Mv(from string, to string) error {
|
||||||
|
return NewError("", 401)
|
||||||
|
}
|
||||||
|
func (b Nothing) Touch(path string) error {
|
||||||
|
return NewError("", 401)
|
||||||
|
}
|
||||||
|
func (b Nothing) Save(path string, file io.Reader) error {
|
||||||
|
return NewError("", 401)
|
||||||
|
}
|
||||||
291
server/model/backend/s3.go
Normal file
291
server/model/backend/s3.go
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
|
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var S3Cache AppCache
|
||||||
|
|
||||||
|
type S3Backend struct {
|
||||||
|
client *s3.S3
|
||||||
|
config *aws.Config
|
||||||
|
params map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
S3Cache = NewAppCache(2, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewS3(params map[string]string, app *App) (IBackend, error) {
|
||||||
|
if params["region"] == "" {
|
||||||
|
params["region"] = "us-east-2"
|
||||||
|
}
|
||||||
|
config := &aws.Config{
|
||||||
|
Credentials: credentials.NewStaticCredentials(params["access_key_id"], params["secret_access_key"], ""),
|
||||||
|
S3ForcePathStyle: aws.Bool(true),
|
||||||
|
Region: aws.String(params["region"]),
|
||||||
|
}
|
||||||
|
if params["endpoint"] != "" {
|
||||||
|
config.Endpoint = aws.String(params["endpoint"])
|
||||||
|
}
|
||||||
|
backend := &S3Backend{
|
||||||
|
config: config,
|
||||||
|
params: params,
|
||||||
|
client: s3.New(session.New(config)),
|
||||||
|
}
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) Info() string {
|
||||||
|
return "s3"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) Meta(path string) *Metadata {
|
||||||
|
if path == "/" {
|
||||||
|
return &Metadata{
|
||||||
|
CanCreateFile: NewBool(false),
|
||||||
|
CanRename: NewBool(false),
|
||||||
|
CanMove: NewBool(false),
|
||||||
|
CanUpload: NewBool(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) Ls(path string) ([]os.FileInfo, error) {
|
||||||
|
p := s.path(path)
|
||||||
|
files := make([]os.FileInfo, 0)
|
||||||
|
|
||||||
|
if p.bucket == "" {
|
||||||
|
b, err := s.client.ListBuckets(&s3.ListBucketsInput{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, bucket := range b.Buckets {
|
||||||
|
files = append(files, &File{
|
||||||
|
FName: *bucket.Name,
|
||||||
|
FType: "directory",
|
||||||
|
FTime: bucket.CreationDate.UnixNano() / 1000,
|
||||||
|
CanMove: NewBool(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := s3.New(s.createSession(p.bucket))
|
||||||
|
objs, err := client.ListObjects(&s3.ListObjectsInput{
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
Prefix: aws.String(p.path),
|
||||||
|
Delimiter: aws.String("/"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, object := range objs.Contents {
|
||||||
|
files = append(files, &File{
|
||||||
|
FName: filepath.Base(*object.Key),
|
||||||
|
FType: "file",
|
||||||
|
FTime: object.LastModified.UnixNano() / 1000,
|
||||||
|
FSize: *object.Size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, object := range objs.CommonPrefixes {
|
||||||
|
files = append(files, &File{
|
||||||
|
FName: filepath.Base(*object.Prefix),
|
||||||
|
FType: "directory",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) Cat(path string) (io.Reader, error) {
|
||||||
|
p := s.path(path)
|
||||||
|
client := s3.New(s.createSession(p.bucket))
|
||||||
|
|
||||||
|
obj, err := client.GetObject(&s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
Key: aws.String(p.path),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) Mkdir(path string) error {
|
||||||
|
p := s.path(path)
|
||||||
|
client := s3.New(s.createSession(p.bucket))
|
||||||
|
|
||||||
|
if p.path == "" {
|
||||||
|
_, err := client.CreateBucket(&s3.CreateBucketInput{
|
||||||
|
Bucket: aws.String(path),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := client.PutObject(&s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
Key: aws.String(p.path),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) Rm(path string) error {
|
||||||
|
p := s.path(path)
|
||||||
|
client := s3.New(s.createSession(p.bucket))
|
||||||
|
|
||||||
|
if p.bucket == "" {
|
||||||
|
return NewError("Doesn't exist", 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
objs, err := client.ListObjects(&s3.ListObjectsInput{
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
Prefix: aws.String(p.path),
|
||||||
|
Delimiter: aws.String("/"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, obj := range objs.Contents {
|
||||||
|
_, err := client.DeleteObject(&s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
Key: obj.Key,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pref := range objs.CommonPrefixes {
|
||||||
|
s.Rm("/" + p.bucket + "/" + *pref.Prefix)
|
||||||
|
_, err := client.DeleteObject(&s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
Key: pref.Prefix,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.path == "" {
|
||||||
|
_, err := client.DeleteBucket(&s3.DeleteBucketInput{
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = client.DeleteObject(&s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
Key: aws.String(p.path),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) Mv(from string, to string) error {
|
||||||
|
f := s.path(from)
|
||||||
|
t := s.path(to)
|
||||||
|
client := s3.New(s.createSession(f.bucket))
|
||||||
|
|
||||||
|
if f.path == "" {
|
||||||
|
return NewError("Can't move this", 403)
|
||||||
|
}
|
||||||
|
_, err := client.CopyObject(&s3.CopyObjectInput{
|
||||||
|
Bucket: aws.String(t.bucket),
|
||||||
|
CopySource: aws.String(f.bucket + "/" + f.path),
|
||||||
|
Key: aws.String(t.path),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.Rm(from)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) Touch(path string) error {
|
||||||
|
p := s.path(path)
|
||||||
|
client := s3.New(s.createSession(p.bucket))
|
||||||
|
|
||||||
|
if p.bucket == "" {
|
||||||
|
return NewError("Can't do that on S3", 403)
|
||||||
|
}
|
||||||
|
_, err := client.PutObject(&s3.PutObjectInput{
|
||||||
|
Body: strings.NewReader(""),
|
||||||
|
ContentLength: aws.Int64(0),
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
Key: aws.String(p.path),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) Save(path string, file io.Reader) error {
|
||||||
|
p := s.path(path)
|
||||||
|
|
||||||
|
if p.bucket == "" {
|
||||||
|
return NewError("Can't do that on S3", 403)
|
||||||
|
}
|
||||||
|
uploader := s3manager.NewUploader(s.createSession(path))
|
||||||
|
_, err := uploader.Upload(&s3manager.UploadInput{
|
||||||
|
Body: file,
|
||||||
|
Bucket: aws.String(p.bucket),
|
||||||
|
Key: aws.String(p.path),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) createSession(bucket string) *session.Session {
|
||||||
|
params := s.params
|
||||||
|
params["bucket"] = bucket
|
||||||
|
c := S3Cache.Get(params)
|
||||||
|
if c == nil {
|
||||||
|
res, err := s.client.GetBucketLocation(&s3.GetBucketLocationInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.config.Region = aws.String("us-east-1")
|
||||||
|
} else {
|
||||||
|
if res.LocationConstraint == nil {
|
||||||
|
s.config.Region = aws.String("us-east-1")
|
||||||
|
} else {
|
||||||
|
s.config.Region = res.LocationConstraint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
S3Cache.Set(params, s.config.Region)
|
||||||
|
} else {
|
||||||
|
s.config.Region = c.(*string)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(s.config)
|
||||||
|
return sess
|
||||||
|
}
|
||||||
|
|
||||||
|
type S3Path struct {
|
||||||
|
bucket string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S3Backend) path(p string) S3Path {
|
||||||
|
sp := strings.Split(p, "/")
|
||||||
|
bucket := ""
|
||||||
|
if len(sp) > 1 {
|
||||||
|
bucket = sp[1]
|
||||||
|
}
|
||||||
|
path := ""
|
||||||
|
if len(sp) > 2 {
|
||||||
|
path = strings.Join(sp[2:], "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return S3Path{
|
||||||
|
bucket,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
// https://www.npmjs.com/package/aws-sdk
|
|
||||||
var AWS = require('aws-sdk');
|
|
||||||
|
|
||||||
|
|
||||||
function decode(path){
|
|
||||||
let tmp = path.split('/');
|
|
||||||
return {
|
|
||||||
bucket: tmp.splice(0, 2)[1] || null,
|
|
||||||
path: tmp.join('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect(params){
|
|
||||||
let config = {
|
|
||||||
apiVersion: '2006-03-01',
|
|
||||||
accessKeyId: params.access_key_id,
|
|
||||||
secretAccessKey: params.secret_access_key,
|
|
||||||
signatureVersion: 'v4',
|
|
||||||
s3ForcePathStyle: true,
|
|
||||||
//sslEnabled: true
|
|
||||||
};
|
|
||||||
if(params.endpoint){
|
|
||||||
config.endpoint = new AWS.Endpoint(params.endpoint);
|
|
||||||
}
|
|
||||||
var s3 = new AWS.S3(config);
|
|
||||||
return Promise.resolve(s3);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
test: function(params){
|
|
||||||
return connect(params)
|
|
||||||
.then((s3) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
s3.listBuckets(function(error, data) {
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{ done(params) }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
cat: function(path, params, res){
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((s3) => {
|
|
||||||
return Promise.resolve(s3.getObject({
|
|
||||||
Bucket: path.bucket,
|
|
||||||
Key: path.path
|
|
||||||
}).on('httpHeaders', function (statusCode, headers) {
|
|
||||||
res.set('content-type', headers['content-type']);
|
|
||||||
res.set('content-length', headers['content-length']);
|
|
||||||
res.set('last-modified', headers['last-modified']);
|
|
||||||
}).createReadStream())
|
|
||||||
});
|
|
||||||
},
|
|
||||||
ls: function(path, params){
|
|
||||||
if(/\/$/.test(path) === false) path += '/';
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((s3) => {
|
|
||||||
if(path.bucket === null){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
s3.listBuckets(function(error, data) {
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{
|
|
||||||
let buckets = data.Buckets.map((bucket) => {
|
|
||||||
return {
|
|
||||||
name: bucket.Name,
|
|
||||||
type: 'bucket',
|
|
||||||
time: new Date(bucket.CreationDate).getTime(),
|
|
||||||
can_read: true,
|
|
||||||
can_delete: true,
|
|
||||||
can_move: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
buckets.push({type: 'metadata', name: './', can_create_file: false, can_create_directory: true});
|
|
||||||
done(buckets)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
s3.listObjects({
|
|
||||||
Bucket: path.bucket,
|
|
||||||
Prefix: path.path,
|
|
||||||
Delimiter: '/'
|
|
||||||
}, function(error, data) {
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{
|
|
||||||
let content = data.Contents
|
|
||||||
.filter((file) => {
|
|
||||||
return file.Key === path.path? false : true;
|
|
||||||
})
|
|
||||||
.map((file) => {
|
|
||||||
return {
|
|
||||||
type: 'file',
|
|
||||||
size: file.Size,
|
|
||||||
time: new Date(file.LastModified).getTime(),
|
|
||||||
name: file.Key.split('/').pop()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let folders = data.CommonPrefixes.map((prefix) => {
|
|
||||||
return {
|
|
||||||
type: 'directory',
|
|
||||||
size: 0,
|
|
||||||
time: null,
|
|
||||||
name: prefix.Prefix.split('/').slice(-2)[0]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return done([].concat(folders, content));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
write: function(path, stream, params){
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((s3) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
s3.upload({
|
|
||||||
Bucket: path.bucket,
|
|
||||||
Key: path.path,
|
|
||||||
Body: stream,
|
|
||||||
ContentLength: stream.byteCount
|
|
||||||
}, function(error, data) {
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{
|
|
||||||
done('ok');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
rm: function(path, params){
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((s3) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
s3.listObjects({
|
|
||||||
Bucket: path.bucket,
|
|
||||||
Prefix: path.path
|
|
||||||
}, function(error, obj){
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{
|
|
||||||
Promise.all(obj.Contents.map((file) => {
|
|
||||||
return deleteObject(s3, path.bucket, file.Key)
|
|
||||||
})).then(function(){
|
|
||||||
if(path.path === ''){
|
|
||||||
s3.deleteBucket({
|
|
||||||
Bucket: path.bucket
|
|
||||||
}, function(error){
|
|
||||||
if(error){ err(error)}
|
|
||||||
else{ done('ok'); }
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
done('ok');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function deleteObject(s3, bucket, key){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
s3.deleteObject({
|
|
||||||
Bucket: bucket,
|
|
||||||
Key: key
|
|
||||||
}, function(error, data) {
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{ done('ok') }
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mv: function(from, to, params){
|
|
||||||
from = decode(from);
|
|
||||||
to = decode(to);
|
|
||||||
|
|
||||||
return connect(params)
|
|
||||||
.then((s3) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
s3.copyObject({
|
|
||||||
Bucket: to.bucket,
|
|
||||||
CopySource: from.bucket+'/'+from.path,
|
|
||||||
Key: to.path
|
|
||||||
}, function(error, data) {
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{
|
|
||||||
s3.deleteObject({
|
|
||||||
Bucket: from.bucket,
|
|
||||||
Key: from.path
|
|
||||||
}, function(error){
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{
|
|
||||||
done('ok');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mkdir: function(path, params){
|
|
||||||
if(/\/$/.test(path) === false) path += '/';
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((s3) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
if(path.path === ''){
|
|
||||||
s3.createBucket({
|
|
||||||
Bucket: path.bucket
|
|
||||||
}, function(error, data){
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{ done('ok') }
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
s3.putObject({
|
|
||||||
Bucket: path.bucket,
|
|
||||||
Key: path.path
|
|
||||||
}, function(error, data) {
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{ done('ok') }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
touch: function(path, params){
|
|
||||||
path = decode(path);
|
|
||||||
return connect(params)
|
|
||||||
.then((s3) => {
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
s3.putObject({
|
|
||||||
Bucket: path.bucket,
|
|
||||||
Key: path.path,
|
|
||||||
Body: ''
|
|
||||||
}, function(error, data) {
|
|
||||||
if(error){ err(error) }
|
|
||||||
else{ done('ok') }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
266
server/model/backend/sftp.go
Normal file
266
server/model/backend/sftp.go
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"github.com/pkg/sftp"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SftpCache AppCache
|
||||||
|
|
||||||
|
type Sftp struct {
|
||||||
|
SSHClient *ssh.Client
|
||||||
|
SFTPClient *sftp.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SftpCache = NewAppCache()
|
||||||
|
|
||||||
|
SftpCache.OnEvict(func(key string, value interface{}) {
|
||||||
|
c := value.(*Sftp)
|
||||||
|
c.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSftp(params map[string]string, app *App) (*Sftp, error) {
|
||||||
|
var s Sftp = Sftp{}
|
||||||
|
p := struct {
|
||||||
|
hostname string
|
||||||
|
port string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
passphrase string
|
||||||
|
}{
|
||||||
|
params["hostname"],
|
||||||
|
params["port"],
|
||||||
|
params["username"],
|
||||||
|
params["password"],
|
||||||
|
params["passphrase"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.port == "" {
|
||||||
|
p.port = "22"
|
||||||
|
}
|
||||||
|
|
||||||
|
c := SftpCache.Get(params)
|
||||||
|
if c != nil {
|
||||||
|
d := c.(*Sftp)
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := p.hostname + ":" + p.port
|
||||||
|
var auth []ssh.AuthMethod
|
||||||
|
isPrivateKey := func(pass string) bool {
|
||||||
|
if len(pass) > 1000 && strings.HasPrefix(pass, "-----") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPrivateKey(p.password) {
|
||||||
|
signer, err := ssh.ParsePrivateKeyWithPassphrase([]byte(p.password), []byte(p.passphrase))
|
||||||
|
if err == nil {
|
||||||
|
auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auth = []ssh.AuthMethod{ssh.Password(p.password)}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
User: p.username,
|
||||||
|
Auth: auth,
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
client, err := ssh.Dial("tcp", addr, config)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
return &s, NewError("Connection denied", 502)
|
||||||
|
}
|
||||||
|
s.SSHClient = client
|
||||||
|
|
||||||
|
session, err := sftp.NewClient(s.SSHClient)
|
||||||
|
if err != nil {
|
||||||
|
return &s, NewError("Can't establish connection", 502)
|
||||||
|
}
|
||||||
|
s.SFTPClient = session
|
||||||
|
SftpCache.Set(params, &s)
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Info() string {
|
||||||
|
return "sftp"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Home() (string, error) {
|
||||||
|
cwd, err := b.SFTPClient.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", b.err(err)
|
||||||
|
}
|
||||||
|
length := len(cwd)
|
||||||
|
if length > 0 && cwd[length-1:] != "/" {
|
||||||
|
return cwd + "/", nil
|
||||||
|
}
|
||||||
|
return cwd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Ls(path string) ([]os.FileInfo, error) {
|
||||||
|
files, err := b.SFTPClient.ReadDir(path)
|
||||||
|
return files, b.err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Cat(path string) (io.Reader, error) {
|
||||||
|
remoteFile, err := b.SFTPClient.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, b.err(err)
|
||||||
|
}
|
||||||
|
return remoteFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Mkdir(path string) error {
|
||||||
|
err := b.SFTPClient.Mkdir(path)
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Rm(path string) error {
|
||||||
|
if IsDirectory(path) {
|
||||||
|
list, err := b.SFTPClient.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
for _, entry := range list {
|
||||||
|
p := path + entry.Name()
|
||||||
|
if entry.IsDir() {
|
||||||
|
p += "/"
|
||||||
|
err := b.Rm(p)
|
||||||
|
if err != nil {
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := b.SFTPClient.Remove(p)
|
||||||
|
if err != nil {
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = b.SFTPClient.RemoveDirectory(path)
|
||||||
|
if err != nil {
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := b.SFTPClient.Remove(path)
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Mv(from string, to string) error {
|
||||||
|
err := b.SFTPClient.Rename(from, to)
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Touch(path string) error {
|
||||||
|
file, err := b.SFTPClient.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
_, err = file.ReadFrom(strings.NewReader(""))
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Save(path string, file io.Reader) error {
|
||||||
|
remoteFile, err := b.SFTPClient.OpenFile(path, os.O_WRONLY|os.O_CREATE)
|
||||||
|
if err != nil {
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
_, err = remoteFile.ReadFrom(file)
|
||||||
|
return b.err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) Close() error {
|
||||||
|
err0 := b.SFTPClient.Close()
|
||||||
|
err1 := b.SSHClient.Close()
|
||||||
|
|
||||||
|
if err0 != nil {
|
||||||
|
return err0
|
||||||
|
}
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Sftp) err(e error) error {
|
||||||
|
f, ok := e.(*sftp.StatusError)
|
||||||
|
if ok == false {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
switch f.Code {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
return NewError("There's nothing more to see", 404)
|
||||||
|
case 2:
|
||||||
|
return NewError("Does not exist", 404)
|
||||||
|
case 3:
|
||||||
|
return NewError("Permission denied", 403)
|
||||||
|
case 4:
|
||||||
|
return NewError("Failure", 400)
|
||||||
|
case 5:
|
||||||
|
return NewError("Not Compatible", 400)
|
||||||
|
case 6:
|
||||||
|
return NewError("No Connection", 503)
|
||||||
|
case 7:
|
||||||
|
return NewError("Connection Lost", 503)
|
||||||
|
case 8:
|
||||||
|
return NewError("Operation not supported", 501)
|
||||||
|
case 9:
|
||||||
|
return NewError("Not valid", 400)
|
||||||
|
case 10:
|
||||||
|
return NewError("No such path", 404)
|
||||||
|
case 11:
|
||||||
|
return NewError("File already exists", 409)
|
||||||
|
case 12:
|
||||||
|
return NewError("Write protected", 403)
|
||||||
|
case 13:
|
||||||
|
return NewError("No media", 404)
|
||||||
|
case 14:
|
||||||
|
return NewError("No space left", 400)
|
||||||
|
case 15:
|
||||||
|
return NewError("Quota exceeded", 400)
|
||||||
|
case 16:
|
||||||
|
return NewError("Unknown", 400)
|
||||||
|
case 17:
|
||||||
|
return NewError("Lock conflict", 409)
|
||||||
|
case 18:
|
||||||
|
return NewError("Directory not empty", 400)
|
||||||
|
case 19:
|
||||||
|
return NewError("Not a directory", 400)
|
||||||
|
case 20:
|
||||||
|
return NewError("Invalid filename", 400)
|
||||||
|
case 21:
|
||||||
|
return NewError("Link loop", 508)
|
||||||
|
case 22:
|
||||||
|
return NewError("Cannot delete", 400)
|
||||||
|
case 23:
|
||||||
|
return NewError("Invalid query", 400)
|
||||||
|
case 24:
|
||||||
|
return NewError("File is a directory", 400)
|
||||||
|
case 25:
|
||||||
|
return NewError("Lock conflict", 409)
|
||||||
|
case 26:
|
||||||
|
return NewError("Lock refused", 400)
|
||||||
|
case 27:
|
||||||
|
return NewError("Delete pending", 400)
|
||||||
|
case 28:
|
||||||
|
return NewError("File corrupt", 400)
|
||||||
|
case 29:
|
||||||
|
return NewError("Invalid owner", 400)
|
||||||
|
case 30:
|
||||||
|
return NewError("Invalid group", 400)
|
||||||
|
case 31:
|
||||||
|
return NewError("Lock wasn't granted", 400)
|
||||||
|
default:
|
||||||
|
return NewError("Oops! Something went wrong", 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
var Client = require('ssh2-sftp-client');
|
|
||||||
|
|
||||||
const connections = {};
|
|
||||||
setInterval(() => {
|
|
||||||
for(let key in connections){
|
|
||||||
if(connections[key].date + (1000*120) < new Date().getTime()){
|
|
||||||
connections[key].conn.end();
|
|
||||||
delete connections[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
|
|
||||||
function connect(params){
|
|
||||||
if(connections[JSON.stringify(params)]){
|
|
||||||
connections[JSON.stringify(params)].date = new Date().getTime();
|
|
||||||
return Promise.resolve(connections[JSON.stringify(params)].conn);
|
|
||||||
}else{
|
|
||||||
let sftp = new Client();
|
|
||||||
let opts = {host: params.host, port: params.port || 22, username: params.username};
|
|
||||||
if(params.hasOwnProperty('private_key') && params['private_key']){
|
|
||||||
opts.privateKey = params['private_key']
|
|
||||||
}else{
|
|
||||||
opts.password = params['password'];
|
|
||||||
}
|
|
||||||
return sftp.connect(opts).then((res) => {
|
|
||||||
connections[JSON.stringify(params)] = {
|
|
||||||
date: new Date().getTime(),
|
|
||||||
conn: sftp
|
|
||||||
}
|
|
||||||
return Promise.resolve(sftp)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = {
|
|
||||||
test: function(params){
|
|
||||||
return connect(params)
|
|
||||||
.then(() => Promise.resolve(params))
|
|
||||||
},
|
|
||||||
cat: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((sftp) => sftp.get(path, false, null));
|
|
||||||
},
|
|
||||||
ls: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((sftp) => sftp.list(path))
|
|
||||||
.then((res) => Promise.resolve(res.map((file) => {
|
|
||||||
return {
|
|
||||||
type: function(type){
|
|
||||||
if(type === 'd'){
|
|
||||||
return 'directory'
|
|
||||||
}else if(type === 'l'){
|
|
||||||
return 'link';
|
|
||||||
}else if(type === '-'){
|
|
||||||
return 'file';
|
|
||||||
}else{
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
}(file.type),
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
time: file.modifyTime
|
|
||||||
};
|
|
||||||
})));
|
|
||||||
},
|
|
||||||
write: function(path, content, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((sftp) => sftp.put(content, path))
|
|
||||||
},
|
|
||||||
rm: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((sftp) => {
|
|
||||||
return sftp.delete(path)
|
|
||||||
.catch((err) => sftp.rmdir(path, true))
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mv: function(from, to, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((sftp) => sftp.rename(from, to));
|
|
||||||
},
|
|
||||||
mkdir: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((sftp) => sftp.mkdir(path, false))
|
|
||||||
},
|
|
||||||
touch: function(path, params){
|
|
||||||
return connect(params)
|
|
||||||
.then((sftp) => sftp.put(Buffer.from(''), path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
206
server/model/backend/webdav.go
Normal file
206
server/model/backend/webdav.go
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebDav struct {
|
||||||
|
params *WebDavParams
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebDavParams struct {
|
||||||
|
url string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebDav(params map[string]string, app *App) (IBackend, error) {
|
||||||
|
params["url"] = regexp.MustCompile(`\/$`).ReplaceAllString(params["url"], "")
|
||||||
|
backend := WebDav{
|
||||||
|
params: &WebDavParams{
|
||||||
|
params["url"],
|
||||||
|
params["username"],
|
||||||
|
params["password"],
|
||||||
|
params["path"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebDav) Info() string {
|
||||||
|
return "webdav"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebDav) Ls(path string) ([]os.FileInfo, error) {
|
||||||
|
files := make([]os.FileInfo, 0)
|
||||||
|
query := `<d:propfind xmlns:d='DAV:'>
|
||||||
|
<d:prop>
|
||||||
|
<d:displayname/>
|
||||||
|
<d:resourcetype/>
|
||||||
|
<d:getlastmodified/>
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>`
|
||||||
|
res, err := w.request("PROPFIND", w.params.url+encodeURL(path), strings.NewReader(query), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return nil, NewError(HTTPFriendlyStatus(res.StatusCode)+": can't get things in "+filepath.Base(path), res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r WebDavResp
|
||||||
|
decoder := xml.NewDecoder(res.Body)
|
||||||
|
decoder.Decode(&r)
|
||||||
|
if len(r.Responses) == 0 {
|
||||||
|
return nil, NewError("Server not found", 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
URLDav := regexp.MustCompile(`^http[s]?://[^/]*`).ReplaceAllString(w.params.url+encodeURL(path), "")
|
||||||
|
for _, tag := range r.Responses {
|
||||||
|
if tag.Href == URLDav {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, prop := range tag.Props {
|
||||||
|
if i > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t, _ := time.Parse(time.RFC1123Z, prop.Modified)
|
||||||
|
files = append(files, File{
|
||||||
|
FName: func(p string) string {
|
||||||
|
name := filepath.Base(p)
|
||||||
|
name = decodeURL(name)
|
||||||
|
return name
|
||||||
|
}(tag.Href),
|
||||||
|
FType: func(p string) string {
|
||||||
|
if p == "collection" {
|
||||||
|
return "directory"
|
||||||
|
}
|
||||||
|
return "file"
|
||||||
|
}(prop.Type.Local),
|
||||||
|
FTime: t.UnixNano() / 1000,
|
||||||
|
FSize: int64(prop.Size),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebDav) Cat(path string) (io.Reader, error) {
|
||||||
|
res, err := w.request("GET", w.params.url+encodeURL(path), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return nil, NewError(HTTPFriendlyStatus(res.StatusCode)+": can't create "+filepath.Base(path), res.StatusCode)
|
||||||
|
}
|
||||||
|
return res.Body, nil
|
||||||
|
}
|
||||||
|
func (w WebDav) Mkdir(path string) error {
|
||||||
|
res, err := w.request("MKCOL", w.params.url+encodeURL(path), nil, func(req *http.Request) {
|
||||||
|
req.Header.Add("Overwrite", "F")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't create "+filepath.Base(path), res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (w WebDav) Rm(path string) error {
|
||||||
|
res, err := w.request("DELETE", w.params.url+encodeURL(path), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't remove "+filepath.Base(path), res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (w WebDav) Mv(from string, to string) error {
|
||||||
|
res, err := w.request("MOVE", w.params.url+encodeURL(from), nil, func(req *http.Request) {
|
||||||
|
req.Header.Add("Destination", w.params.url+encodeURL(to))
|
||||||
|
req.Header.Add("Overwrite", "T")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't do that", res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (w WebDav) Touch(path string) error {
|
||||||
|
return w.Save(path, strings.NewReader(""))
|
||||||
|
}
|
||||||
|
func (w WebDav) Save(path string, file io.Reader) error {
|
||||||
|
res, err := w.request("PUT", w.params.url+encodeURL(path), file, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return NewError(HTTPFriendlyStatus(res.StatusCode)+": can't do that", res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebDav) request(method string, url string, body io.Reader, fn func(req *http.Request)) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if w.params.username != "" {
|
||||||
|
req.SetBasicAuth(w.params.username, w.params.password)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "text/xml;charset=UTF-8")
|
||||||
|
req.Header.Add("Accept", "application/xml,text/xml")
|
||||||
|
req.Header.Add("Accept-Charset", "utf-8")
|
||||||
|
|
||||||
|
if req.Body != nil {
|
||||||
|
defer req.Body.Close()
|
||||||
|
}
|
||||||
|
if fn != nil {
|
||||||
|
fn(req)
|
||||||
|
}
|
||||||
|
return HTTPClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebDavResp struct {
|
||||||
|
Responses []struct {
|
||||||
|
Href string `xml:"href"`
|
||||||
|
Props []struct {
|
||||||
|
Name string `xml:"prop>displayname,omitempty"`
|
||||||
|
Type xml.Name `xml:"prop>resourcetype>collection,omitempty"`
|
||||||
|
Size int64 `xml:"prop>getcontentlength,omitempty"`
|
||||||
|
Modified string `xml:"prop>getlastmodified,omitempty"`
|
||||||
|
} `xml:"propstat"`
|
||||||
|
} `xml:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeURL(path string) string {
|
||||||
|
p := url.PathEscape(path)
|
||||||
|
return strings.Replace(p, "%2F", "/", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeURL(path string) string {
|
||||||
|
str, err := url.PathUnescape(path)
|
||||||
|
if err != nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
var fs = require("webdav-fs");
|
|
||||||
var Readable = require('stream').Readable;
|
|
||||||
var toString = require('stream-to-string');
|
|
||||||
|
|
||||||
function connect(params){
|
|
||||||
return fs(
|
|
||||||
params.url,
|
|
||||||
params.username,
|
|
||||||
params.password
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
test: function(params){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
connect(params).readFile('/', function(error, res){
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done(params); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
cat: function(path, params){
|
|
||||||
return Promise.resolve(connect(params).createReadStream(path));
|
|
||||||
},
|
|
||||||
ls: function(path, params){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
connect(params).readdir(path, function(error, contents) {
|
|
||||||
if (!error) {
|
|
||||||
done(contents.map((content) => {
|
|
||||||
return {
|
|
||||||
name: content.name,
|
|
||||||
type: function(cont){
|
|
||||||
if(cont.isDirectory()){
|
|
||||||
return 'directory';
|
|
||||||
}else if(cont.isFile()){
|
|
||||||
return 'file'
|
|
||||||
}else{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}(content),
|
|
||||||
time: content.mtime,
|
|
||||||
size: content.size
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
err(error);
|
|
||||||
}
|
|
||||||
}, 'stat');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
write: function(path, content, params){
|
|
||||||
return Promise.resolve(content.pipe(connect(params).createWriteStream(path)));
|
|
||||||
},
|
|
||||||
rm: function(path, params){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
connect(params).unlink(path, function (error) {
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done('ok'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mv: function(from, to, params){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
connect(params).rename(from, to, function (error) {
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done('ok'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mkdir: function(path, params){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
connect(params).mkdir(path, function(error) {
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done('done'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
touch: function(path, params){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
connect(params).writeFile(path, '', function(error) {
|
|
||||||
if(error){ err(error); }
|
|
||||||
else{ done('done'); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
server/model/files.go
Normal file
37
server/model/files.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"github.com/mickael-kerjean/nuage/server/model/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewBackend(ctx *App, conn map[string]string) (IBackend, error) {
|
||||||
|
switch conn["type"] {
|
||||||
|
case "webdav":
|
||||||
|
return backend.NewWebDav(conn, ctx)
|
||||||
|
case "ftp":
|
||||||
|
return backend.NewFtp(conn, ctx)
|
||||||
|
case "sftp":
|
||||||
|
return backend.NewSftp(conn, ctx)
|
||||||
|
case "git":
|
||||||
|
return backend.NewGit(conn, ctx)
|
||||||
|
case "s3":
|
||||||
|
return backend.NewS3(conn, ctx)
|
||||||
|
case "dropbox":
|
||||||
|
return backend.NewDropbox(conn, ctx)
|
||||||
|
case "gdrive":
|
||||||
|
return backend.NewGDrive(conn, ctx)
|
||||||
|
default:
|
||||||
|
return backend.NewNothing(conn, ctx)
|
||||||
|
}
|
||||||
|
return nil, NewError("Invalid backend type", 501)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHome(b IBackend) (string, error) {
|
||||||
|
obj, ok := b.(interface{ Home() (string, error) })
|
||||||
|
if ok == false {
|
||||||
|
_, err := b.Ls("/")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return obj.Home()
|
||||||
|
}
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
var backend = {
|
|
||||||
ftp: require('./backend/ftp'),
|
|
||||||
sftp: require('./backend/sftp'),
|
|
||||||
webdav: require('./backend/webdav'),
|
|
||||||
dropbox: require('./backend/dropbox'),
|
|
||||||
gdrive: require('./backend/gdrive'),
|
|
||||||
s3: require('./backend/s3'),
|
|
||||||
git: require('./backend/git')
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.cat = function(path, params, res){
|
|
||||||
try{
|
|
||||||
if(backend[params.type] && typeof backend[params.type].cat === 'function'){
|
|
||||||
return backend[params.type].cat(path, params.payload, res);
|
|
||||||
}else{
|
|
||||||
return error('not implemented');
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
return error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.write = function(path, content, params){
|
|
||||||
try{
|
|
||||||
if(backend[params.type] && typeof backend[params.type].write === 'function'){
|
|
||||||
return backend[params.type].write(path, content, params.payload);
|
|
||||||
}else{
|
|
||||||
return error('not implemented');
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
return error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.ls = function(path, params){
|
|
||||||
try{
|
|
||||||
if(backend[params.type] && typeof backend[params.type].ls === 'function'){
|
|
||||||
return backend[params.type].ls(path, params.payload);
|
|
||||||
}else{
|
|
||||||
return error('not implemented');
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
return error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.mv = function(from, to, params){
|
|
||||||
try{
|
|
||||||
if(backend[params.type] && typeof backend[params.type].mv === 'function'){
|
|
||||||
return backend[params.type].mv(from, to, params.payload);
|
|
||||||
}else{
|
|
||||||
return error('not implemented');
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
return error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.rm = function(path, params){
|
|
||||||
try{
|
|
||||||
if(backend[params.type] && typeof backend[params.type].rm === 'function'){
|
|
||||||
return backend[params.type].rm(path, params.payload);
|
|
||||||
}else{
|
|
||||||
return error('not implemented');
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
return error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.mkdir = function(path, params){
|
|
||||||
try{
|
|
||||||
if(backend[params.type] && typeof backend[params.type].mkdir === 'function'){
|
|
||||||
return backend[params.type].mkdir(path, params.payload);
|
|
||||||
}else{
|
|
||||||
return error('not implemented');
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
return error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.touch = function(path, params){
|
|
||||||
try{
|
|
||||||
if(backend[params.type] && typeof backend[params.type].touch === 'function'){
|
|
||||||
return backend[params.type].touch(path, params.payload);
|
|
||||||
}else{
|
|
||||||
return error('not implemented');
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
return error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function error(message){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
err(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
262
server/model/files_test.go
Normal file
262
server/model/files_test.go
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var app *App
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
app = &App{}
|
||||||
|
app.Config = &Config{}
|
||||||
|
app.Config.Initialise()
|
||||||
|
app.Config.General.Host = "http://test"
|
||||||
|
app.Config.OAuthProvider.Dropbox.ClientID = ""
|
||||||
|
app.Config.OAuthProvider.GoogleDrive.ClientID = ""
|
||||||
|
app.Config.OAuthProvider.GoogleDrive.ClientID = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebdav(t *testing.T) {
|
||||||
|
if os.Getenv("WEBDAV_URL") == "" {
|
||||||
|
fmt.Println("- skipped webdav")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := NewBackend(&App{}, map[string]string{
|
||||||
|
"type": "webdav",
|
||||||
|
"url": os.Getenv("WEBDAV_URL"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Can't create WebDav backend")
|
||||||
|
}
|
||||||
|
setup(t, b)
|
||||||
|
suite(t, b)
|
||||||
|
tearDown(t, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFtp(t *testing.T) {
|
||||||
|
if os.Getenv("FTP_USERNAME") == "" || os.Getenv("FTP_PASSWORD") == "" {
|
||||||
|
fmt.Println("- skipped ftp")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := NewBackend(&App{}, map[string]string{
|
||||||
|
"type": "ftp",
|
||||||
|
"hostname": "127.0.0.1",
|
||||||
|
"username": os.Getenv("FTP_USERNAME"),
|
||||||
|
"password": os.Getenv("FTP_PASSWORD"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Can't create FTP backend")
|
||||||
|
}
|
||||||
|
setup(t, b)
|
||||||
|
suite(t, b)
|
||||||
|
tearDown(t, b)
|
||||||
|
b.Rm("/tmp/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSFtp(t *testing.T) {
|
||||||
|
if os.Getenv("SFTP_USERNAME") == "" || os.Getenv("SFTP_PASSWORD") == "" {
|
||||||
|
fmt.Println("- skipped sftp")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := NewBackend(&App{}, map[string]string{
|
||||||
|
"type": "sftp",
|
||||||
|
"hostname": "127.0.0.1",
|
||||||
|
"username": os.Getenv("SFTP_USERNAME"),
|
||||||
|
"password": os.Getenv("SFTP_PASSWORD"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Can't create SFTP backend")
|
||||||
|
}
|
||||||
|
setup(t, b)
|
||||||
|
suite(t, b)
|
||||||
|
tearDown(t, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGit(t *testing.T) {
|
||||||
|
if os.Getenv("GIT_USERNAME") == "" || os.Getenv("GIT_PASSWORD") == "" {
|
||||||
|
fmt.Println("- skipped git")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := NewBackend(app, map[string]string{
|
||||||
|
"type": "git",
|
||||||
|
"repo": "https://github.com/mickael-kerjean/tmp",
|
||||||
|
"username": os.Getenv("GIT_EMAIL"),
|
||||||
|
"password": os.Getenv("GIT_PASSWORD"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Can't create Git backend")
|
||||||
|
}
|
||||||
|
setup(t, b)
|
||||||
|
suite(t, b)
|
||||||
|
tearDown(t, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestS3(t *testing.T) {
|
||||||
|
if os.Getenv("S3_ID") == "" || os.Getenv("S3_SECRET") == "" {
|
||||||
|
fmt.Println("- skipped S3")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := NewBackend(&App{}, map[string]string{
|
||||||
|
"type": "s3",
|
||||||
|
"access_key_id": os.Getenv("S3_ID"),
|
||||||
|
"secret_access_key": os.Getenv("S3_SECRET"),
|
||||||
|
"endpoint": os.Getenv("S3_ENDPOINT"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Can't create S3 backend")
|
||||||
|
}
|
||||||
|
setup(t, b)
|
||||||
|
suite(t, b)
|
||||||
|
tearDown(t, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDropbox(t *testing.T) {
|
||||||
|
if os.Getenv("DROPBOX_TOKEN") == "" {
|
||||||
|
fmt.Println("- skipped Dropbox")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := NewBackend(app, map[string]string{
|
||||||
|
"type": "dropbox",
|
||||||
|
"bearer": os.Getenv("DROPBOX_TOKEN"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Can't create a Dropbox backend")
|
||||||
|
}
|
||||||
|
setup(t, b)
|
||||||
|
suite(t, b)
|
||||||
|
tearDown(t, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoogleDrive(t *testing.T) {
|
||||||
|
if os.Getenv("GDRIVE_TOKEN") == "" {
|
||||||
|
fmt.Println("- skipped Google Drive")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := NewBackend(app, map[string]string{
|
||||||
|
"type": "gdrive",
|
||||||
|
"expiry": "",
|
||||||
|
"token": os.Getenv("GDRIVE_TOKEN"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Can't create a Google Drive backend")
|
||||||
|
}
|
||||||
|
setup(t, b)
|
||||||
|
suite(t, b)
|
||||||
|
tearDown(t, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(t *testing.T, b IBackend) {
|
||||||
|
b.Rm("/tmp/test/")
|
||||||
|
b.Mkdir("/tmp/")
|
||||||
|
b.Mkdir("/tmp/test/")
|
||||||
|
}
|
||||||
|
func tearDown(t *testing.T, b IBackend) {
|
||||||
|
b.Rm("/tmp/test/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func suite(t *testing.T, b IBackend) {
|
||||||
|
// create state
|
||||||
|
content := "lorem ipsum"
|
||||||
|
b.Mkdir("/tmp/test/trash/")
|
||||||
|
b.Touch("/tmp/test/test0.txt")
|
||||||
|
b.Save("/tmp/test/test0.txt", strings.NewReader(content))
|
||||||
|
b.Save("/tmp/test/test1.txt", strings.NewReader(content))
|
||||||
|
b.Touch("/tmp/test/test2.txt")
|
||||||
|
b.Mv("/tmp/test/test0.txt", "/tmp/test/trash/test0.txt")
|
||||||
|
|
||||||
|
// list all files
|
||||||
|
tmp0, err := b.Ls("/tmp/test/")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Ls error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(tmp0) != 3 {
|
||||||
|
t.Errorf("LS error: got: %d elmnt, want: %d", len(tmp0), 3)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// read file
|
||||||
|
tmp1, err := b.Cat("/tmp/test/trash/test0.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cat error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tmp2, err := ioutil.ReadAll(tmp1)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cat error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(tmp2) != content {
|
||||||
|
t.Errorf("Incorrect file: %s, want: %s.", tmp2, content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if obj, ok := tmp1.(interface{ Close() error }); ok {
|
||||||
|
obj.Close()
|
||||||
|
}
|
||||||
|
tmp1, err = b.Cat("/tmp/test/test1.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cat error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tmp2, err = ioutil.ReadAll(tmp1)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cat error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(tmp2) != content {
|
||||||
|
t.Errorf("Incorrect file: %s, want: %s.", tmp2, content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if obj, ok := tmp1.(interface{ Close() error }); ok {
|
||||||
|
obj.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp1, err = b.Cat("/tmp/test/test2.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cat error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tmp2, err = ioutil.ReadAll(tmp1)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cat error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(tmp2) != "" {
|
||||||
|
t.Errorf("Incorrect file: %s, want: %s.", tmp2, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if obj, ok := tmp1.(interface{ Close() error }); ok {
|
||||||
|
obj.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove file
|
||||||
|
b.Rm("/tmp/test/test2.txt")
|
||||||
|
tmp0, err = b.Ls("/tmp/test/")
|
||||||
|
if len(tmp0) != 2 {
|
||||||
|
t.Errorf("Test folder elements, got: %d, want: %d.", len(tmp0), 2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp0, err = b.Ls("/tmp/test/")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Ls error %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(tmp0) != 2 {
|
||||||
|
t.Errorf("LS error: got: %d elmnt, want: %d", len(tmp0), 2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove folder
|
||||||
|
b.Rm("/tmp/test/")
|
||||||
|
tmp0, err = b.Ls("/tmp/test/")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Removed folder still exists: %d", len(tmp0))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
var backend = {
|
|
||||||
ftp: require('./backend/ftp'),
|
|
||||||
sftp: require('./backend/sftp'),
|
|
||||||
webdav: require('./backend/webdav'),
|
|
||||||
dropbox: require('./backend/dropbox'),
|
|
||||||
gdrive: require('./backend/gdrive'),
|
|
||||||
s3: require('./backend/s3'),
|
|
||||||
git: require('./backend/git')
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.test = function(params){
|
|
||||||
try{
|
|
||||||
if(backend[params.type] && typeof backend[params.type].test === 'function'){
|
|
||||||
return backend[params.type].test(params);
|
|
||||||
}else{
|
|
||||||
return error('not implemented');
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
return error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.auth = function(params){
|
|
||||||
try{
|
|
||||||
if(backend[params.type] && typeof backend[params.type].auth === 'function'){
|
|
||||||
return backend[params.type].auth(params);
|
|
||||||
}else{
|
|
||||||
return error('not implemented');
|
|
||||||
}
|
|
||||||
}catch(err){
|
|
||||||
return error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function error(message){
|
|
||||||
return new Promise((done, err) => {
|
|
||||||
err(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
16
server/router/config.go
Normal file
16
server/router/config.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConfigHandler(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
c, err := ctx.Config.Export()
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.Write([]byte("window.CONFIG = "))
|
||||||
|
res.Write([]byte(c))
|
||||||
|
}
|
||||||
195
server/router/files.go
Normal file
195
server/router/files.go
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"github.com/mickael-kerjean/nuage/server/services"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := ctx.Backend.Ls(path)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []FileInfo{}
|
||||||
|
for _, entry := range entries {
|
||||||
|
f := FileInfo{
|
||||||
|
Name: entry.Name(),
|
||||||
|
Size: entry.Size(),
|
||||||
|
Time: func(t time.Time) int64 {
|
||||||
|
return t.UnixNano() / int64(time.Millisecond)
|
||||||
|
}(entry.ModTime()),
|
||||||
|
Type: func(isDir bool) string {
|
||||||
|
if isDir == true {
|
||||||
|
return "directory"
|
||||||
|
}
|
||||||
|
return "file"
|
||||||
|
}(entry.IsDir()),
|
||||||
|
}
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
var perms *Metadata
|
||||||
|
if obj, ok := ctx.Backend.(interface{ Meta(path string) *Metadata }); ok {
|
||||||
|
perms = obj.Meta(path)
|
||||||
|
}
|
||||||
|
sendSuccessResultsWithMetadata(res, files, perms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := ctx.Backend.Cat(path)
|
||||||
|
if obj, ok := file.(interface{ Close() error }); ok {
|
||||||
|
defer obj.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(res, &http.Cookie{
|
||||||
|
Name: "download",
|
||||||
|
Value: "",
|
||||||
|
MaxAge: -1,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
|
||||||
|
file, err = services.ProcessFileBeforeSend(file, &ctx, req, &res)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
io.Copy(res, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileSave(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, _, err := req.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
err = ctx.Backend.Save(path, file)
|
||||||
|
if obj, ok := file.(interface{ Close() error }); ok {
|
||||||
|
obj.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, NewError(err.Error(), 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendSuccessResult(res, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileMv(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
from, err := pathBuilder(ctx, req.URL.Query().Get("from"))
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
to, err := pathBuilder(ctx, req.URL.Query().Get("to"))
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if from == "" || to == "" {
|
||||||
|
sendErrorResult(res, NewError("missing path parameter", 400))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.Backend.Mv(from, to)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendSuccessResult(res, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileRm(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ctx.Backend.Rm(path)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendSuccessResult(res, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileMkdir(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.Backend.Mkdir(path)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendSuccessResult(res, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileTouch(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.Backend.Touch(path)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendSuccessResult(res, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathBuilder(ctx App, path string) (string, error) {
|
||||||
|
if path == "" {
|
||||||
|
return "", NewError("No path available", 400)
|
||||||
|
}
|
||||||
|
basePath := ctx.Session["path"]
|
||||||
|
basePath = filepath.Join(basePath, path)
|
||||||
|
if string(path[len(path)-1]) == "/" && basePath != "/" {
|
||||||
|
basePath += "/"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(basePath, ctx.Session["path"]) == false {
|
||||||
|
return "", NewError("There's nothing here", 403)
|
||||||
|
}
|
||||||
|
return basePath, nil
|
||||||
|
}
|
||||||
44
server/router/index.go
Normal file
44
server/router/index.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mickael-kerjean/mux"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(a *App) *http.Server {
|
||||||
|
r := mux.NewRouter()
|
||||||
|
|
||||||
|
session := r.PathPrefix("/api/session").Subrouter()
|
||||||
|
session.HandleFunc("", APIHandler(SessionIsValid, *a)).Methods("GET")
|
||||||
|
session.HandleFunc("", APIHandler(SessionAuthenticate, *a)).Methods("POST")
|
||||||
|
session.HandleFunc("", APIHandler(SessionLogout, *a)).Methods("DELETE")
|
||||||
|
session.Handle("/auth/{service}", APIHandler(SessionOAuthBackend, *a)).Methods("GET")
|
||||||
|
|
||||||
|
files := r.PathPrefix("/api/files").Subrouter()
|
||||||
|
files.HandleFunc("/ls", APIHandler(LoggedInOnly(FileLs), *a)).Methods("GET")
|
||||||
|
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileCat), *a)).Methods("GET")
|
||||||
|
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileSave), *a)).Methods("POST")
|
||||||
|
files.HandleFunc("/mv", APIHandler(LoggedInOnly(FileMv), *a)).Methods("GET")
|
||||||
|
files.HandleFunc("/rm", APIHandler(LoggedInOnly(FileRm), *a)).Methods("GET")
|
||||||
|
files.HandleFunc("/mkdir", APIHandler(LoggedInOnly(FileMkdir), *a)).Methods("GET")
|
||||||
|
files.HandleFunc("/touch", APIHandler(LoggedInOnly(FileTouch), *a)).Methods("GET")
|
||||||
|
|
||||||
|
r.HandleFunc("/api/config", CtxInjector(ConfigHandler, *a))
|
||||||
|
|
||||||
|
r.PathPrefix("/assets").Handler(StaticHandler("./data/public/", *a))
|
||||||
|
r.NotFoundHandler = IndexHandler("./data/public/index.html", *a)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":" + strconv.Itoa(a.Config.General.Port),
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return srv
|
||||||
|
}
|
||||||
179
server/router/middleware.go
Normal file
179
server/router/middleware.go
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"github.com/mickael-kerjean/nuage/server/model"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.HandlerFunc {
|
||||||
|
return func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
ctx.Body, _ = extractBody(req)
|
||||||
|
ctx.Session, _ = extractSession(req, &ctx)
|
||||||
|
ctx.Backend, _ = extractBackend(req, &ctx)
|
||||||
|
res.Header().Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resw := ResponseWriter{ResponseWriter: res}
|
||||||
|
fn(ctx, &resw, req)
|
||||||
|
req.Body.Close()
|
||||||
|
|
||||||
|
if ctx.Config.Log.Telemetry {
|
||||||
|
go telemetry(req, &resw, start, ctx.Backend.Info())
|
||||||
|
}
|
||||||
|
if ctx.Config.Log.Enable {
|
||||||
|
go logger(req, &resw, start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoggedInOnly(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) {
|
||||||
|
if ctx.Backend == nil || ctx.Session == nil {
|
||||||
|
sendErrorResult(res, NewError("Forbidden", 403))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(ctx, res, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CtxInjector(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
fn(ctx, res, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractBody(req *http.Request) (map[string]string, error) {
|
||||||
|
var body map[string]string
|
||||||
|
if strings.HasPrefix(req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||||
|
return body, NewError("", 200)
|
||||||
|
}
|
||||||
|
byt, err := ioutil.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(byt, &body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSession(req *http.Request, ctx *App) (map[string]string, error) {
|
||||||
|
cookie, err := req.Cookie(COOKIE_NAME)
|
||||||
|
if err != nil {
|
||||||
|
return make(map[string]string), err
|
||||||
|
}
|
||||||
|
return decrypt(ctx.Config.General.SecretKey, cookie.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractBackend(req *http.Request, ctx *App) (IBackend, error) {
|
||||||
|
return model.NewBackend(ctx, ctx.Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func telemetry(req *http.Request, res *ResponseWriter, start time.Time, backendType string) {
|
||||||
|
if os.Getenv("ENV") != "dev" {
|
||||||
|
point := logPoint(req, res, start, backendType)
|
||||||
|
body, err := json.Marshal(point)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formData := bytes.NewReader(body)
|
||||||
|
|
||||||
|
r, _ := http.NewRequest("POST", "https://log.kerjean.me/nuage", formData)
|
||||||
|
r.Header.Set("Content-Type", "application/json")
|
||||||
|
HTTP.Do(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logger(req *http.Request, res *ResponseWriter, start time.Time) {
|
||||||
|
point := logPoint(req, res, start, "")
|
||||||
|
log.Printf("%s %d %d %s %s\n", "INFO", point.Duration, point.Status, point.Method, point.RequestURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logPoint(req *http.Request, res *ResponseWriter, start time.Time, backendType string) *LogEntry {
|
||||||
|
return &LogEntry{
|
||||||
|
Version: APP_VERSION,
|
||||||
|
Scheme: req.URL.Scheme,
|
||||||
|
Host: req.Host,
|
||||||
|
Method: req.Method,
|
||||||
|
RequestURI: req.RequestURI,
|
||||||
|
Proto: req.Proto,
|
||||||
|
Status: res.status,
|
||||||
|
UserAgent: req.Header.Get("User-Agent"),
|
||||||
|
Ip: req.RemoteAddr,
|
||||||
|
Referer: req.Referer(),
|
||||||
|
Duration: int64(time.Now().Sub(start) / (1000 * 1000)),
|
||||||
|
Timestamp: time.Now().UTC(),
|
||||||
|
Backend: backendType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encrypt(keystr string, text map[string]string) (string, error) {
|
||||||
|
key := []byte(keystr)
|
||||||
|
plaintext, err := json.Marshal(text)
|
||||||
|
if err != nil {
|
||||||
|
return "", NewError("json marshalling: "+err.Error(), 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", NewError("encryption issue (cipher): "+err.Error(), 500)
|
||||||
|
}
|
||||||
|
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
|
||||||
|
iv := ciphertext[:aes.BlockSize]
|
||||||
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||||
|
return "", NewError("encryption issue: "+err.Error(), 500)
|
||||||
|
}
|
||||||
|
stream := cipher.NewCFBEncrypter(block, iv)
|
||||||
|
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
|
||||||
|
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decrypt(keystr string, cryptoText string) (map[string]string, error) {
|
||||||
|
var raw map[string]string
|
||||||
|
|
||||||
|
key := []byte(keystr)
|
||||||
|
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
|
||||||
|
if err != nil || len(ciphertext) < aes.BlockSize {
|
||||||
|
return raw, NewError("Cipher is too short", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := ciphertext[:aes.BlockSize]
|
||||||
|
ciphertext = ciphertext[aes.BlockSize:]
|
||||||
|
stream := cipher.NewCFBDecrypter(block, iv)
|
||||||
|
stream.XORKeyStream(ciphertext, ciphertext)
|
||||||
|
|
||||||
|
json.Unmarshal(ciphertext, &raw)
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWriter) WriteHeader(status int) {
|
||||||
|
w.status = status
|
||||||
|
w.ResponseWriter.WriteHeader(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
if w.status == 0 {
|
||||||
|
w.status = 200
|
||||||
|
}
|
||||||
|
return w.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
115
server/router/session.go
Normal file
115
server/router/session.go
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/mickael-kerjean/mux"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"github.com/mickael-kerjean/nuage/server/model"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
COOKIE_NAME = "auth"
|
||||||
|
COOKIE_PATH = "/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SessionIsValid(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
if ctx.Backend == nil {
|
||||||
|
sendSuccessResult(res, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := ctx.Backend.Ls("/"); err != nil {
|
||||||
|
sendSuccessResult(res, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
home, _ := model.GetHome(ctx.Backend)
|
||||||
|
if home == "" {
|
||||||
|
sendSuccessResult(res, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendSuccessResult(res, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
ctx.Body["timestamp"] = time.Now().String()
|
||||||
|
backend, err := model.NewBackend(&ctx, ctx.Body)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj, ok := backend.(interface {
|
||||||
|
OAuthToken(*map[string]string) error
|
||||||
|
}); ok {
|
||||||
|
err := obj.OAuthToken(&ctx.Body)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, NewError("Can't authenticate (OAuth error)", 401))
|
||||||
|
}
|
||||||
|
backend, err = model.NewBackend(&ctx, ctx.Body)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, NewError("Can't authenticate", 401))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
home, err := model.GetHome(backend)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obfuscate, err := encrypt(ctx.Config.General.SecretKey, ctx.Body)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, NewError(err.Error(), 500))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cookie := http.Cookie{
|
||||||
|
Name: COOKIE_NAME,
|
||||||
|
Value: obfuscate,
|
||||||
|
MaxAge: 60 * 60 * 24 * 30,
|
||||||
|
Path: COOKIE_PATH,
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
http.SetCookie(res, &cookie)
|
||||||
|
|
||||||
|
if home == "" {
|
||||||
|
sendSuccessResult(res, nil)
|
||||||
|
} else {
|
||||||
|
sendSuccessResult(res, home)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SessionLogout(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
cookie := http.Cookie{
|
||||||
|
Name: COOKIE_NAME,
|
||||||
|
Value: "",
|
||||||
|
Path: COOKIE_PATH,
|
||||||
|
MaxAge: -1,
|
||||||
|
}
|
||||||
|
if ctx.Backend != nil {
|
||||||
|
if obj, ok := ctx.Backend.(interface{ Close() error }); ok {
|
||||||
|
go obj.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(res, &cookie)
|
||||||
|
sendSuccessResult(res, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SessionOAuthBackend(ctx App, res http.ResponseWriter, req *http.Request) {
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
a := map[string]string{
|
||||||
|
"type": vars["service"],
|
||||||
|
}
|
||||||
|
b, err := model.NewBackend(&ctx, a)
|
||||||
|
if err != nil {
|
||||||
|
sendErrorResult(res, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
obj, ok := b.(interface{ OAuthURL() string })
|
||||||
|
if ok == false {
|
||||||
|
sendErrorResult(res, errors.New("No backend authentication"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendSuccessResult(res, obj.OAuthURL())
|
||||||
|
}
|
||||||
42
server/router/static.go
Normal file
42
server/router/static.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StaticHandler(_path string, ctx App) http.Handler {
|
||||||
|
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
absPath := ctx.Helpers.AbsolutePath(_path)
|
||||||
|
fsrv := http.FileServer(http.Dir(absPath))
|
||||||
|
_, err := os.Open(path.Join(absPath, req.URL.Path+".gz"))
|
||||||
|
|
||||||
|
mType := mime.TypeByExtension(filepath.Ext(req.URL.Path))
|
||||||
|
res.Header().Set("Content-Type", mType)
|
||||||
|
|
||||||
|
if err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
res.Header().Set("Content-Encoding", "gzip")
|
||||||
|
req.URL.Path += ".gz"
|
||||||
|
}
|
||||||
|
res.Header().Set("Cache-Control", "max-age=2592000")
|
||||||
|
fsrv.ServeHTTP(res, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexHandler(_path string, ctx App) http.Handler {
|
||||||
|
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
res.Header().Set("Content-Type", "text/html")
|
||||||
|
|
||||||
|
p := _path
|
||||||
|
if _, err := os.Open(path.Join(ctx.Config.Runtime.Dirname, p+".gz")); err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
res.Header().Set("Content-Encoding", "gzip")
|
||||||
|
p += ".gz"
|
||||||
|
}
|
||||||
|
http.ServeFile(res, req, ctx.Helpers.AbsolutePath(p))
|
||||||
|
})
|
||||||
|
}
|
||||||
62
server/router/utils.go
Normal file
62
server/router/utils.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APISuccessResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Result interface{} `json:"result,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APISuccessResults struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Results interface{} `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APISuccessResultsWithMetadata struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Results interface{} `json:"results"`
|
||||||
|
Metadata interface{} `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIErrorMessage struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendSuccessResult(res http.ResponseWriter, data interface{}) {
|
||||||
|
encoder := json.NewEncoder(res)
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
encoder.Encode(APISuccessResult{"ok", data})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendSuccessResults(res http.ResponseWriter, data interface{}) {
|
||||||
|
encoder := json.NewEncoder(res)
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
encoder.Encode(APISuccessResults{"ok", data})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendSuccessResultsWithMetadata(res http.ResponseWriter, data interface{}, p interface{}) {
|
||||||
|
encoder := json.NewEncoder(res)
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
encoder.Encode(APISuccessResultsWithMetadata{"ok", data, p})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendErrorResult(res http.ResponseWriter, err error) {
|
||||||
|
encoder := json.NewEncoder(res)
|
||||||
|
encoder.SetEscapeHTML(false)
|
||||||
|
obj, ok := err.(interface{ Status() int })
|
||||||
|
if ok == true {
|
||||||
|
res.WriteHeader(obj.Status())
|
||||||
|
}
|
||||||
|
m := func(r string) string {
|
||||||
|
if r == "" {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return strings.ToUpper(string(r[0])) + string(r[1:])
|
||||||
|
}(err.Error())
|
||||||
|
encoder.Encode(APIErrorMessage{"error", m})
|
||||||
|
}
|
||||||
67
server/services/images/raw.c
Normal file
67
server/services/images/raw.c
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <libraw/libraw.h>
|
||||||
|
|
||||||
|
int save_thumbnail(const char *filename, libraw_data_t *raw){
|
||||||
|
int err;
|
||||||
|
err = libraw_dcraw_thumb_writer(raw, filename);
|
||||||
|
libraw_close(raw);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
int raw_process(const char* filename, int min_width){
|
||||||
|
int err;
|
||||||
|
libraw_data_t *raw;
|
||||||
|
int thumbnail_working = 0;
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// boot up libraw
|
||||||
|
raw = libraw_init(0);
|
||||||
|
if(libraw_open_file(raw, filename) != 0){
|
||||||
|
libraw_close(raw);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
raw->params.output_tiff = 1;
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// use thumbnail if available
|
||||||
|
if(libraw_unpack_thumb(raw) == 0){
|
||||||
|
thumbnail_working = 1;
|
||||||
|
if(raw->thumbnail.twidth > min_width && raw->thumbnail.tformat == LIBRAW_THUMBNAIL_JPEG){
|
||||||
|
return save_thumbnail(filename, raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// transcode image
|
||||||
|
if(libraw_unpack(raw) != 0){
|
||||||
|
if(thumbnail_working == 1){
|
||||||
|
return save_thumbnail(filename, raw);
|
||||||
|
}
|
||||||
|
libraw_close(raw);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = libraw_dcraw_process(raw);
|
||||||
|
if(err != 0){
|
||||||
|
if(err == LIBRAW_UNSUFFICIENT_MEMORY){
|
||||||
|
libraw_close(raw);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if(thumbnail_working == 1){
|
||||||
|
return save_thumbnail(filename, raw);
|
||||||
|
}
|
||||||
|
libraw_close(raw);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(libraw_dcraw_ppm_tiff_writer(raw, filename) != 0){
|
||||||
|
if(thumbnail_working == 1){
|
||||||
|
return save_thumbnail(filename, raw);
|
||||||
|
}
|
||||||
|
libraw_close(raw);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
libraw_close(raw);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
65
server/services/images/raw.go
Normal file
65
server/services/images/raw.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package images
|
||||||
|
|
||||||
|
// #cgo pkg-config: libraw
|
||||||
|
// #include <raw.h>
|
||||||
|
// #include <stdlib.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const LIBRAW_MEMORY_ERROR = -1
|
||||||
|
|
||||||
|
func IsRaw(mType string) bool {
|
||||||
|
switch mType {
|
||||||
|
case "image/x-tif":
|
||||||
|
case "image/x-canon-cr2":
|
||||||
|
case "image/x-canon-crw":
|
||||||
|
case "image/x-nikon-nef":
|
||||||
|
case "image/x-nikon-nrw":
|
||||||
|
case "image/x-sony-arw":
|
||||||
|
case "image/x-sony-sr2":
|
||||||
|
case "image/x-minolta-mrw":
|
||||||
|
case "image/x-minolta-mdc":
|
||||||
|
case "image/x-olympus-orf":
|
||||||
|
case "image/x-panasonic-rw2":
|
||||||
|
case "image/x-pentax-pef":
|
||||||
|
case "image/x-epson-erf":
|
||||||
|
case "image/x-raw":
|
||||||
|
case "image/x-x3f":
|
||||||
|
case "image/x-fuji-raf":
|
||||||
|
case "image/x-aptus-mos":
|
||||||
|
case "image/x-mamiya-mef":
|
||||||
|
case "image/x-hasselblad-3fr":
|
||||||
|
case "image/x-adobe-dng":
|
||||||
|
case "image/x-samsung-srw":
|
||||||
|
case "image/x-kodak-kdc":
|
||||||
|
case "image/x-kodak-dcr":
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractPreview(t *Transform) error {
|
||||||
|
filename := C.CString(t.Temporary)
|
||||||
|
err := C.raw_process(filename, C.int(t.Size))
|
||||||
|
if err == LIBRAW_MEMORY_ERROR {
|
||||||
|
// libraw acts weird sometimes and I couldn't
|
||||||
|
// find a way to increase its available memory :(
|
||||||
|
r := rand.Intn(2000) + 500
|
||||||
|
time.Sleep(time.Duration(r) * time.Millisecond)
|
||||||
|
C.free(unsafe.Pointer(filename))
|
||||||
|
return ExtractPreview(t)
|
||||||
|
} else if err != 0 {
|
||||||
|
C.free(unsafe.Pointer(filename))
|
||||||
|
return NewError("", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
C.free(unsafe.Pointer(filename))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
4
server/services/images/raw.h
Normal file
4
server/services/images/raw.h
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <libraw/libraw.h>
|
||||||
|
|
||||||
|
int raw_process(const char* filename, int min_width);
|
||||||
36
server/services/images/resizer.c
Normal file
36
server/services/images/resizer.c
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <vips/vips.h>
|
||||||
|
|
||||||
|
int resizer_init(const int ncpu, const int cache_max, const int cache_mem){
|
||||||
|
if(VIPS_INIT("nuage")){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
vips_concurrency_set(ncpu);
|
||||||
|
vips_cache_set_max(cache_max);
|
||||||
|
vips_cache_set_max_mem(cache_mem);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int resizer_process(const char *filename, void **buf, size_t *len, int size, int crop, int quality, int exif){
|
||||||
|
VipsImage *img;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
size = size > 4000 || size < 0 ? 1000 : size;
|
||||||
|
crop = crop == 0 ? VIPS_INTERESTING_NONE : VIPS_INTERESTING_CENTRE;
|
||||||
|
quality = quality > 100 || quality < 0 ? 80 : quality;
|
||||||
|
exif = exif == 0 ? TRUE : FALSE;
|
||||||
|
|
||||||
|
err = vips_thumbnail(filename, &img, size,
|
||||||
|
"size", VIPS_SIZE_DOWN,
|
||||||
|
"auto_rotate", TRUE,
|
||||||
|
"crop", crop,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
if(err != 0){
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = vips_jpegsave_buffer(img, buf, len, "Q", quality, "strip", exif, NULL);
|
||||||
|
g_object_unref(img);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
57
server/services/images/resizer.go
Normal file
57
server/services/images/resizer.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package images
|
||||||
|
|
||||||
|
// #cgo pkg-config: vips
|
||||||
|
// #include <resizer.h>
|
||||||
|
// #include <stdlib.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var LIBVIPS_INSTALLED = false
|
||||||
|
|
||||||
|
type Transform struct {
|
||||||
|
Temporary string
|
||||||
|
Size int
|
||||||
|
Crop bool
|
||||||
|
Quality int
|
||||||
|
Exif bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if C.resizer_init(C.int(runtime.NumCPU()), 50, 1024) != 0 {
|
||||||
|
log.Println("WARNING Can't load libvips")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
LIBVIPS_INSTALLED = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateThumbnail(t *Transform) (io.Reader, error) {
|
||||||
|
if LIBVIPS_INSTALLED == false {
|
||||||
|
return nil, NewError("Libvips not installed", 501)
|
||||||
|
}
|
||||||
|
filename := C.CString(t.Temporary)
|
||||||
|
defer C.free(unsafe.Pointer(filename))
|
||||||
|
var buffer unsafe.Pointer
|
||||||
|
len := C.size_t(0)
|
||||||
|
|
||||||
|
if C.resizer_process(filename, &buffer, &len, C.int(t.Size), boolToCInt(t.Crop), C.int(t.Quality), boolToCInt(t.Exif)) != 0 {
|
||||||
|
return nil, NewError("", 500)
|
||||||
|
}
|
||||||
|
buf := C.GoBytes(buffer, C.int(len))
|
||||||
|
C.g_free(C.gpointer(buffer))
|
||||||
|
return bytes.NewReader(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToCInt(val bool) C.int {
|
||||||
|
if val == false {
|
||||||
|
return C.int(0)
|
||||||
|
}
|
||||||
|
return C.int(1)
|
||||||
|
}
|
||||||
6
server/services/images/resizer.h
Normal file
6
server/services/images/resizer.h
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <vips/vips.h>
|
||||||
|
|
||||||
|
int resizer_init(const int ncpu, const int cache_max, const int cache_mem);
|
||||||
|
|
||||||
|
int resizer_process(const char *filename, void **buf, size_t *len, int size, int crop, int quality, int exif);
|
||||||
92
server/services/pipeline.go
Normal file
92
server/services/pipeline.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/mickael-kerjean/nuage/server/common"
|
||||||
|
"github.com/mickael-kerjean/nuage/server/services/images"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ImageCachePath = "data/cache/image/"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cachePath := filepath.Join(GetCurrentDir(), ImageCachePath)
|
||||||
|
os.RemoveAll(cachePath)
|
||||||
|
os.MkdirAll(cachePath, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessFileBeforeSend(reader io.Reader, ctx *App, req *http.Request, res *http.ResponseWriter) (io.Reader, error) {
|
||||||
|
query := req.URL.Query()
|
||||||
|
mType := ctx.Helpers.MimeType(query.Get("path"))
|
||||||
|
(*res).Header().Set("Content-Type", mType)
|
||||||
|
|
||||||
|
if strings.HasPrefix(mType, "image/") {
|
||||||
|
if query.Get("thumbnail") != "true" && query.Get("size") == "" {
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// Specify transformation
|
||||||
|
transform := &images.Transform{
|
||||||
|
Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + RandomString(10)),
|
||||||
|
Size: 300,
|
||||||
|
Crop: true,
|
||||||
|
Quality: 50,
|
||||||
|
Exif: false,
|
||||||
|
}
|
||||||
|
if query.Get("thumbnail") == "true" {
|
||||||
|
(*res).Header().Set("Cache-Control", "max-age=259200")
|
||||||
|
} else if query.Get("size") != "" {
|
||||||
|
(*res).Header().Set("Cache-Control", "max-age=600")
|
||||||
|
size, err := strconv.ParseInt(query.Get("size"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
transform.Size = int(size)
|
||||||
|
transform.Crop = false
|
||||||
|
transform.Quality = 90
|
||||||
|
transform.Exif = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// Insert file in the fs
|
||||||
|
// => 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)
|
||||||
|
}
|
||||||
|
io.Copy(file, reader)
|
||||||
|
file.Close()
|
||||||
|
if obj, ok := reader.(interface{ Close() error }); ok {
|
||||||
|
obj.Close()
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
os.Remove(transform.Temporary)
|
||||||
|
}()
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// Transcode RAW image
|
||||||
|
if images.IsRaw(mType) {
|
||||||
|
if images.ExtractPreview(transform) == nil {
|
||||||
|
mType = "image/jpeg"
|
||||||
|
(*res).Header().Set("Content-Type", mType)
|
||||||
|
} else {
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// Final stage: resizing
|
||||||
|
if mType != "image/jpeg" && mType != "image/png" && mType != "image/gif" && mType != "image/tiff" {
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
return images.CreateThumbnail(transform)
|
||||||
|
}
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
module.exports = function(EXPIRE, REFRESH = 60000){
|
|
||||||
let conn = {};
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
for(let key in conn){
|
|
||||||
if(conn[key] && conn[key].date + EXPIRE * 1000 > new Date().getTime()){
|
|
||||||
file.rm(key).then(() => delete conn[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, REFRESH);
|
|
||||||
|
|
||||||
return {
|
|
||||||
get: function(key){
|
|
||||||
if(conn[key] && new Date().getTime() > conn[key].date + CACHE_TIMEOUT * 1000){
|
|
||||||
return conn[key].data;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
put: function(key, data){
|
|
||||||
conn[key] = {
|
|
||||||
date: new Date(),
|
|
||||||
data: data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
const crypto = require('crypto'),
|
|
||||||
algorithm = 'aes-256-cbc',
|
|
||||||
password = require('../../config_server')['secret_key'];
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
encrypt: function(obj){
|
|
||||||
obj.date = new Date().getTime();
|
|
||||||
const text = JSON.stringify(obj);
|
|
||||||
const cipher = crypto.createCipher(algorithm, password);
|
|
||||||
let crypted = cipher.update(text, 'utf8', 'base64');
|
|
||||||
crypted += cipher.final('base64');
|
|
||||||
return crypted;
|
|
||||||
},
|
|
||||||
decrypt: function(text){
|
|
||||||
var dec;
|
|
||||||
try{
|
|
||||||
const decipher = crypto.createDecipher(algorithm, password);
|
|
||||||
dec = decipher.update(text, 'base64', 'utf8');
|
|
||||||
dec += decipher.final('utf8');
|
|
||||||
dec = JSON.parse(dec);
|
|
||||||
}catch(err){
|
|
||||||
dec = null;
|
|
||||||
}
|
|
||||||
return dec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue