feature (share): share feature - WIP

This commit is contained in:
Mickael Kerjean 2018-09-05 15:52:15 +10:00 committed by Mickael KERJEAN
parent d1686c3aa2
commit 61f28962f3
16 changed files with 341 additions and 63 deletions

View file

@ -4,8 +4,13 @@ class ShareModel {
constructor(){}
all(path = "/"){
const url = `api/share?path=${path}`;
return http_get(url);
const url = `/api/share?path=${path}`;
return http_get(url).then((res) => res.results);
}
get(id){
const url = `/api/share/${id}`;
return http_get(url).then((res) => res.result);
}
upsert(obj){

View file

@ -13,42 +13,33 @@ export class ShareComponent extends React.Component {
show_advanced: false,
role: null,
id: randomString(7),
existings: [
{id: "dflkjse", role: "UPLOADER", path: "./test/test"},
{id: "dflkjse", role: "VIEWER", path: "./test/test", password: "xxxx"},
{id: "dflkjse", role: "EDITOR", path: "./test/test"},
{id: "dflkjse", role: "VIEWER", path: "./test/test", password: "xxxx"},
{id: "dflkjse", role: "EDITOR", path: "./test/test"},
{id: "dflkjse", role: "UPLOADER", path: "./test/test"},
]
existings: []
};
}
componentDidMount(){
Share.all(this.props.path)
.then((existings) => {
this.refreshModal();
this.setState({existings: existings});
});
}
updateState(key, value){
if(this.state[key] === value){
this.setState({[key]: null});
}else{
this.setState({[key]: value});
}
if(key === "role" && value && window.innerHeight < 500){
window.dispatchEvent(new Event('resize'));
if(key === "role" && value){
this.refreshModal();
}
}
registerLink(e){
e.target.setSelectionRange(0, e.target.value.length);
let st = Object.assign({}, this.state);
delete st.existings;
delete st.show_advanced;
this.setState({existing: [st].concat(this.state.existings)});
return Share.upsert(st)
.catch((err) => {
notify.send(err, "error");
this.setState({
existings: this.state.existings.slice(0, this.state.existings.length)
});
});
refreshModal(){
if(window.innerHeight < 500){
window.dispatchEvent(new Event('resize'));
}
}
onLoad(link){
@ -59,14 +50,44 @@ export class ShareComponent extends React.Component {
this.setState(st);
}
onDelete(link_id){
onDeleteLink(link_id){
let removed = null,
i = 0;
for(i=0; i < this.state.existings.length; i++){
if(this.state.existings[i].id === link_id){
removed = Object.assign({}, this.state.existings[i]);
break;
}
}
if(removed !== null){
this.state.existings.splice(i, 1);
this.setState({existings: this.state.existings});
}
return Share.remove(link_id)
.then(() => {
console.log("HERE");
})
.catch((err) => notify.send(err, "error"));
.catch((err) => {
this.setState({existings: [removed].concat(this.state.existings)});
notify.send(err, "error");
});
}
onRegisterLink(e){
e.target.setSelectionRange(0, e.target.value.length);
let st = Object.assign({}, this.state);
delete st.existings;
delete st.show_advanced;
this.setState({existings: [st].concat(this.state.existings)});
return Share.upsert(st)
.catch((err) => {
notify.send(err, "error");
this.setState({
existings: this.state.existings.slice(0, this.state.existings.length)
});
});
}
render(){
return (
<div className="component_share">
@ -90,10 +111,10 @@ export class ShareComponent extends React.Component {
{
this.state.existings && this.state.existings.map((link, i) => {
return (
<div className="link-details" key={i}>
<div className="link-details" key={link.id}>
<span className="role">{link.role}</span>
<span>{link.path}</span>
<Icon onClick={this.onDelete.bind(this, link.id)} name="delete"/>
<Icon onClick={this.onDeleteLink.bind(this, link.id)} name="delete"/>
<Icon onClick={this.onLoad.bind(this, link)} name="edit"/>
</div>
);
@ -105,7 +126,7 @@ export class ShareComponent extends React.Component {
<NgIf cond={this.state.role !== null}>
<h2>Restrictions</h2>
<div className="share--content advanced-settings no-select">
<SuperCheckbox value={this.state.users} label="Only for users" placeholder="list of users who can access the link" onChange={this.updateState.bind(this, 'users')} inputType="text"/>
<SuperCheckbox value={this.state.users} label="Only for users" placeholder="name0@email.com,name1@email.com" onChange={this.updateState.bind(this, 'users')} inputType="text"/>
<SuperCheckbox value={this.state.password} label="Password" placeholder="protect access with a password" onChange={this.updateState.bind(this, 'password')} inputType="password"/>
</div>
@ -124,7 +145,7 @@ export class ShareComponent extends React.Component {
</div>
<div className="shared-link">
<input onClick={this.registerLink.bind(this)} type="text" value={window.location.origin+"/s/"+(this.state.url || this.state.id)} onChange={() => {}}/>
<input onClick={this.onRegisterLink.bind(this)} type="text" value={window.location.origin+"/s/"+(this.state.url || this.state.id)} onChange={() => {}}/>
</div>
</NgIf>
</div>

View file

@ -48,6 +48,7 @@
color: var(--light);
display: inline-block;
min-width: 75px;
text-transform: uppercase;
}
.component_icon{
width: 20px;

View file

@ -185,7 +185,7 @@ export class ExistingThing extends React.Component {
onShareRequest(filename){
alert.now(
<ShareComponent/>,
<ShareComponent path={this.props.path}/>,
(ok) => {}
);
}
@ -313,7 +313,7 @@ const ActionButton = (props) => {
<NgIf cond={props.can_delete !== false} type="inline">
<Icon name="delete" onClick={onDelete} className="component_updater--icon"/>
</NgIf>
<NgIf cond={props.can_share !== false} type="inline">
<NgIf cond={false && props.can_share !== false} type="inline">
<Icon name="share" onClick={onShare} className="component_updater--icon"/>
</NgIf>
</div>

View file

@ -111,6 +111,7 @@
margin: 2px;
padding: 0;
position: relative;
border: 3px solid transparent;
> span > img{
padding: 0;
margin: 0;

View file

@ -1,4 +1,5 @@
export { HomePage } from './homepage';
export { SharePage } from './sharepage';
export { ConnectPage } from './connectpage';
export { LogoutPage } from './logout';
export { NotFoundPage } from './notfoundpage';

75
client/pages/sharepage.js Normal file
View file

@ -0,0 +1,75 @@
import React from 'react';
import { Redirect } from 'react-router';
import { Share } from '../model/';
import { notify } from '../helpers/';
import { Loader, Input, Button, Container } from '../components/';
import './error.scss';
export class SharePage extends React.Component {
constructor(props){
super(props);
this.state = {
redirection: null,
loading: true,
request_password: false,
request_username: false
};
}
componentDidMount(){
Share.get(this.props.match.params.id)
.then((res) => {
this.setState({
loading: false,
redirection: res
});
})
.catch((res) => {
console.log(">> COMPONENT DID MOUNT:: ", res);
this.setState({
loading: false
});
});
}
render() {
if(this.state.loading === true){
return ( <div> <Loader /> </div> );
}
if(this.state.request_password === true){
return (
<Container maxWidth="350px">
<form style={{marginTop: parseInt(window.innerHeight / 3)+'px'}}>
<Input type="password" placeholder="Password" />
<Button theme="emphasis">OK</Button>
</form>
</Container>
);
}else if(this.state.request_username === true){
return (
<Container maxWidth="350px">
<form style={{marginTop: parseInt(window.innerHeight / 3)+'px'}}>
<Input type="text" placeholder="Your email address" />
<Button theme="emphasis">OK</Button>
</form>
</Container>
);
}
if(this.state.redirection !== null){
if(this.state.redirection.slice(-1) === "/"){
return ( <Redirect to={"/files" + this.state.redirection} /> );
}else{
return ( <Redirect to={"/view" + this.state.redirection} /> );
}
}else{
return (
<div className="error-page">
<h1>Oops!</h1>
<h2>There's nothing in here</h2>
</div>
);
}
}
}

View file

@ -135,6 +135,7 @@
margin: 5px 0;
border-top: 1px dashed var(--color);
padding-top: 5px;
clear: both;
.title{ float: left; margin-right: 5px; }
.value{ color: var(--bg-color); }
}

View file

@ -1,6 +1,6 @@
import React from 'react';
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
import { NotFoundPage, ConnectPage, HomePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
import { NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
import { Bundle, URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
import { ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video } from './components/';
@ -11,6 +11,7 @@ export default class AppRouter extends React.Component {
<BrowserRouter>
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/s/:id*" component={SharePage} />
<Route path="/login" component={ConnectPage} />
<Route path="/files/:path*" component={FilesPage} />
<Route path="/view/:path*" component={ViewerPage} />

View file

@ -4,6 +4,8 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha1"
"encoding/base32"
"encoding/base64"
"encoding/json"
"io"
@ -49,3 +51,18 @@ func Decrypt(keystr string, cryptoText string) (map[string]string, error) {
json.Unmarshal(ciphertext, &raw)
return raw, nil
}
func GenerateID(params map[string]string) string {
p := "type =>" + params["type"]
p += "host =>" + params["host"]
p += "hostname =>" + params["hostname"]
p += "username =>" + params["username"]
p += "repo =>" + params["repo"]
p += "access_key_id =>" + params["access_key_id"]
p += "endpoint =>" + params["endpoint"]
p += "bearer =>" + params["bearer"]
p += "token =>" + params["token"]
hasher := sha1.New()
hasher.Write([]byte(p))
return base32.HexEncoding.EncodeToString(hasher.Sum(nil))
}

View file

@ -17,3 +17,7 @@ func RandomString(n int) string {
func NewBool(t bool) *bool {
return &t
}
func NewString(t string) *string {
return &t
}

View file

@ -8,27 +8,35 @@ import (
)
type ShareAPI struct {
Id string `json:"id"`
Path string `json:"path"`
Role string `json:"role"`
Password string `json:"password"`
Users []string `json:"users"`
CanManageOwn bool `json:"can_manage_own"`
CanShare bool `json:"can_share"`
Expire int `json:"expire"`
Link string `json:"link"`
Id string `json:"id"`
Path string `json:"path"`
Role string `json:"role"`
Password *string `json:"password"`
Users *[]string `json:"users"`
CanManageOwn *bool `json:"can_manage_own"`
CanShare *bool `json:"can_share"`
Expire *int `json:"expire"`
CustomURI *string `json:"uri"`
}
func ShareList(ctx App, res http.ResponseWriter, req *http.Request) {
p := extractParams(req)
listOfSharedLinks := model.ShareList(p)
s := extractParams(req, &ctx)
listOfSharedLinks := model.ShareList(s)
SendSuccessResults(res, listOfSharedLinks)
}
func ShareGet(ctx App, res http.ResponseWriter, req *http.Request) {
s := extractParams(req, &ctx)
if err := model.ShareGet(&s); err != nil {
SendErrorResult(res, err)
return
}
SendSuccessResult(res, s)
}
func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
p := extractParams(req)
err := model.ShareUpsert(p, model.ShareParams{})
if err != nil {
s := extractParams(req, &ctx)
if err := model.ShareUpsert(s); err != nil {
SendErrorResult(res, err)
return
}
@ -36,16 +44,18 @@ func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
}
func ShareDelete(ctx App, res http.ResponseWriter, req *http.Request) {
p := extractParams(req)
err := model.ShareDelete(p)
if err != nil {
s := extractParams(req, &ctx)
if err := model.ShareDelete(s); err != nil {
SendErrorResult(res, err)
return
}
SendSuccessResult(res, nil)
}
func extractParams(req *http.Request) model.ShareKey {
vars := mux.Vars(req)
return model.ShareKey{vars["id"], "", ""}
func extractParams(req *http.Request, ctx *App) model.Share {
return model.Share{
Id: NewString(mux.Vars(req)["id"]),
Backend: NewString(GenerateID(ctx.Session)),
Path: NewString(req.URL.Query().Get("path")),
}
}

View file

@ -3,7 +3,6 @@ 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"
@ -95,10 +94,7 @@ func NewGit(params map[string]string, app *App) (*Git, error) {
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)
}
hash := GenerateID(params)
p.basePath = app.Helpers.AbsolutePath(GitCachePath + "repo_" + fmt.Sprint(hash) + "/")
repo, err := g.git.open(p, p.basePath)

View file

@ -0,0 +1,9 @@
package model
func CanRemoveShare() bool {
return false
}
func CanEditShare() bool {
return false
}

135
server/model/share.go Normal file
View file

@ -0,0 +1,135 @@
package model
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
. "github.com/mickael-kerjean/nuage/server/common"
"log"
"os"
"path/filepath"
)
const DBCachePath = "data/"
type Share struct {
Id *string `json:"id"`
Backend *string `json:"-"`
Path *string `json:"path"`
Params struct {
} `json:"-"`
Role *string `json:"role"`
Password *string `json:"password,omitempty"`
Users *[]string `json:"-"`
Expire *int `json:"expire,omitempty"`
CanRead *bool `json:"-"`
CanWrite *bool `json:"-"`
CanUpload *bool `json:"-"`
CanShare *bool `json:"can_share,omitempty"`
CanManageOwn *bool `json:"can_manage_own,omitempty"`
}
func init() {
cachePath := filepath.Join(GetCurrentDir(), DBCachePath)
os.MkdirAll(cachePath, os.ModePerm)
db, err := sql.Open("sqlite3", cachePath+"/db.sql")
if err != nil {
return
}
stmt, err := db.Prepare("CREATE TABLE IF NOT EXISTS Location(backend VARCHAR(16), path VARCHAR(512), CONSTRAINT pk_location PRIMARY KEY(backend, path))")
if err != nil {
return
}
stmt.Exec()
stmt, err = db.Prepare("CREATE TABLE IF NOT EXISTS Share(id VARCHAR(64) PRIMARY KEY, related_backend VARCHAR(16), related_path VARCHAR(512), params JSON, FOREIGN KEY (related_backend, related_path) REFERENCES Location(backend, path) ON UPDATE CASCADE ON DELETE CASCADE)")
if err != nil {
return
}
stmt.Exec()
}
func ShareList(p Share) []Share {
db, err := getDb()
if err != nil {
return nil
}
log.Println("- backend: ", p.Backend)
stmt, err := db.Prepare("SELECT s.id, l.path, s.params FROM Share as s LEFT JOIN Location as l ON l.backend = s.related_backend")
log.Println("err1:", err)
if err != nil {
return nil
}
rows, err := stmt.Query()
log.Println(">> ROWS::", rows)
log.Println("err2:", err)
if err != nil {
return nil
}
defer rows.Close()
sharedFiles := []Share{}
for rows.Next() {
var a Share
//var params string
rows.Scan(&a.Id, &a.Path, &a.Role)
a.Role = NewString("viewer")
sharedFiles = append(sharedFiles, a)
}
return sharedFiles
}
func ShareGet(p *Share) error {
db, err := getDb()
if err != nil {
return err
}
stmt, err := db.Prepare("SELECT id, related_path, params FROM share WHERE id = ?")
if err != nil {
return err
}
defer stmt.Close()
row := stmt.QueryRow(p.Id)
row.Scan(&p.Id, &p.Path)
return nil
}
func ShareUpsert(p Share) error {
db, err := getDb()
if err != nil {
return err
}
stmt, err := db.Prepare("INSERT INTO Share(id, related_backend, related_path, params) VALUES($1, $2, $3, $4) ON CONFLICT(id) DO UPDATE SET related_backend = $,2s related_path = $3, params = $4")
if err != nil {
return err
}
_, err = stmt.Exec(p.Id, p.Backend, p.Path, "{}")
return err
}
func ShareDelete(p Share) error {
db, err := getDb()
if err != nil {
return err
}
stmt, err := db.Prepare("DELETE FROM Share WHERE id = ?")
if err != nil {
return err
}
_, err = stmt.Exec(p.Id)
return err
}
func getDb() (*sql.DB, error) {
path := filepath.Join(GetCurrentDir(), DBCachePath) + "/db.sql"
return sql.Open("sqlite3", path)
}
func shareToDBParams(s Share) string {
return ""
}
func DPParamstoShare() Share {
return Share{}
}

View file

@ -29,6 +29,7 @@ func Init(a *App) *http.Server {
share := r.PathPrefix("/api/share").Subrouter()
share.HandleFunc("", APIHandler(ShareList, *a)).Methods("GET")
share.HandleFunc("/{id}", APIHandler(ShareGet, *a)).Methods("GET")
share.HandleFunc("/{id}", APIHandler(ShareUpsert, *a)).Methods("POST")
share.HandleFunc("/{id}", APIHandler(ShareDelete, *a)).Methods("DELETE")