pull/211/head
Henrique Dias 2017-08-18 09:00:32 +01:00
parent ec190d28a8
commit 174330929a
No known key found for this signature in database
GPG Key ID: 936F5EB68D786730
19 changed files with 9860 additions and 3002 deletions

37
file.go
View File

@ -27,8 +27,8 @@ var (
errInvalidOption = errors.New("Invalid option")
)
// file contains the information about a particular file or directory.
type file struct {
// File contains the information about a particular file or directory.
type File struct {
// Indicates the Kind of view on the front-end (listing, editor or preview).
Kind string `json:"kind"`
// The name of the file.
@ -63,7 +63,7 @@ type file struct {
// A listing is the context used to fill out a template.
type listing struct {
// The items (files and folders) in the path.
Items []*file `json:"items"`
Items []*File `json:"items"`
// The number of directories in the listing.
NumDirs int `json:"numDirs"`
// The number of files (items that aren't directories) in the listing.
@ -76,12 +76,12 @@ type listing struct {
Display string `json:"display"`
}
// getInfo gets the file information and, in case of error, returns the
// GetInfo gets the file information and, in case of error, returns the
// respective HTTP error code
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
func GetInfo(url *url.URL, c *FileManager, u *User) (*File, error) {
var err error
i := &file{
i := &File{
URL: "/files" + url.String(),
VirtualPath: url.Path,
Path: filepath.Join(string(u.FileSystem), url.Path),
@ -106,11 +106,11 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
return i, nil
}
// getListing gets the information about a specific directory and its files.
func (i *file) getListing(c *RequestContext, r *http.Request) error {
// GetListing gets the information about a specific directory and its files.
func (i *File) GetListing(u *User, r *http.Request) error {
// Gets the directory information using the Virtual File System of
// the user configuration.
f, err := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0)
f, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
if err != nil {
return err
}
@ -123,7 +123,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
}
var (
fileinfos []*file
fileinfos []*File
dirCount, fileCount int
)
@ -134,7 +134,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
for _, f := range files {
name := f.Name()
allowed := c.User.Allowed("/" + name)
allowed := u.Allowed("/" + name)
if !allowed {
continue
@ -150,7 +150,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
// Absolute URL
url := url.URL{Path: baseurl + name}
i := &file{
i := &File{
Name: f.Name(),
Size: f.Size(),
ModTime: f.ModTime(),
@ -175,8 +175,8 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
return nil
}
// getEditor gets the editor based on a Info struct
func (i *file) getEditor() error {
// GetEditor gets the editor based on a Info struct
func (i *File) GetEditor() error {
i.Language = editorLanguage(i.Extension)
// If the editor will hold only content, leave now.
if editorMode(i.Language) == "content" {
@ -205,7 +205,7 @@ func (i *file) getEditor() error {
// GetFileType obtains the mimetype and converts it to a simple
// type nomenclature.
func (i *file) GetFileType(checkContent bool) error {
func (i *File) GetFileType(checkContent bool) error {
var content []byte
var err error
@ -283,7 +283,8 @@ End:
return nil
}
func (i file) Checksum(kind string) (string, error) {
// Checksum retrieves the checksum of a file.
func (i File) Checksum(algo string) (string, error) {
file, err := os.Open(i.Path)
if err != nil {
return "", err
@ -293,7 +294,7 @@ func (i file) Checksum(kind string) (string, error) {
var h hash.Hash
switch kind {
switch algo {
case "md5":
h = md5.New()
case "sha1":
@ -315,7 +316,7 @@ func (i file) Checksum(kind string) (string, error) {
}
// CanBeEdited checks if the extension of a file is supported by the editor
func (i file) CanBeEdited() bool {
func (i File) CanBeEdited() bool {
return i.Type == "text"
}

View File

@ -60,28 +60,14 @@ import (
"os"
"os/exec"
"reflect"
"regexp"
"strings"
"time"
rice "github.com/GeertJohan/go.rice"
"github.com/asdine/storm"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/robfig/cron"
)
var (
errUserExist = errors.New("user already exists")
errUserNotExist = errors.New("user does not exist")
errEmptyRequest = errors.New("request body is empty")
errEmptyPassword = errors.New("password is empty")
errEmptyUsername = errors.New("username is empty")
errEmptyScope = errors.New("scope is empty")
errWrongDataType = errors.New("wrong data type")
errInvalidUpdateField = errors.New("invalid field to update")
)
// FileManager is a file manager instance. It should be creating using the
// 'New' function and not directly.
type FileManager struct {
@ -111,8 +97,6 @@ type FileManager struct {
// there will only exist one user, called "admin".
NoAuth bool
// staticgen is the name of the current static website generator.
staticgen string
// StaticGen is the static websit generator handler.
StaticGen StaticGen
@ -124,82 +108,18 @@ type FileManager struct {
// A map of events to a slice of commands.
Commands map[string][]string
Store *Store
}
type Store struct {
Users *UsersStore
}
// Command is a command function.
type Command func(r *http.Request, m *FileManager, u *User) error
// User contains the configuration for each user.
type User struct {
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// Username is the user username used to login.
Username string `json:"username" storm:"index,unique"`
// The hashed password. This never reaches the front-end because it's temporarily
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// FileSystem is the virtual file system the user has access.
FileSystem fileutils.Dir `json:"filesystem"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
}
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
// DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
FileSystem: fileutils.Dir("."),
}
/*
// New creates a new File Manager instance. If 'database' file already
// exists, it will load the users from there. Otherwise, a new user
@ -308,7 +228,7 @@ func New(database string, base User) (*FileManager, error) {
m.cron.Start()
return m, nil
}
} */
// RootURL returns the actual URL where
// File Manager interface can be accessed.
@ -336,7 +256,7 @@ func (m *FileManager) SetBaseURL(url string) {
// ServeHTTP handles the request.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := serveHTTP(&RequestContext{
/* code, err := serveHTTP(&RequestContext{
FileManager: m,
User: nil,
File: nil,
@ -355,66 +275,32 @@ func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Print(err)
w.Write([]byte(err.Error()))
}
} */
}
// EnableStaticGen attaches a static generator to the current File Manager
// instance.
func (m *FileManager) EnableStaticGen(data StaticGen) error {
if reflect.TypeOf(data).Kind() != reflect.Ptr {
// Attach attaches a static generator to the current File Manager.
func (m *FileManager) Attach(s StaticGen) error {
if reflect.TypeOf(s).Kind() != reflect.Ptr {
return errors.New("data should be a pointer to interface, not interface")
}
if h, ok := data.(*Hugo); ok {
return m.enableHugo(h)
}
if j, ok := data.(*Jekyll); ok {
return m.enableJekyll(j)
}
return errors.New("unknown static website generator")
}
func (m *FileManager) enableHugo(h *Hugo) error {
if err := h.find(); err != nil {
err := s.Setup()
if err != nil {
return err
}
m.staticgen = "hugo"
m.StaticGen = h
m.StaticGen = s
err := m.db.Get("staticgen", "hugo", h)
// TODO: Save...
/* err := m.db.Get("staticgen", "hugo", h)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "hugo", *h)
}
*/
return nil
}
func (m *FileManager) enableJekyll(j *Jekyll) error {
if err := j.find(); err != nil {
return err
}
if len(j.Args) == 0 {
j.Args = []string{"build"}
}
if j.Args[0] != "build" {
j.Args = append([]string{"build"}, j.Args...)
}
m.staticgen = "jekyll"
m.StaticGen = j
err := m.db.Get("staticgen", "jekyll", j)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "jekyll", *j)
}
return nil
}
/*
// shareCleaner removes sharing links that are no longer active.
// This function is set to run periodically.
@ -437,38 +323,7 @@ func (m FileManager) shareCleaner() {
}
}
}
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule
i := len(u.Rules) - 1
for i >= 0 {
rule = u.Rules[i]
if rule.Regex {
if rule.Regexp.MatchString(url) {
return rule.Allow
}
} else if strings.HasPrefix(url, rule.Path) {
return rule.Allow
}
i--
}
return true
}
// MatchString checks if this string matches the regular expression.
func (r *Regexp) MatchString(s string) bool {
if r.regexp == nil {
r.regexp = regexp.MustCompile(r.Raw)
}
return r.regexp.MatchString(s)
}
} */
// Runner runs the commands for a certain event type.
func (m FileManager) Runner(event string, path string) error {

325
http.go
View File

@ -1,320 +1,23 @@
package filemanager
import (
"encoding/json"
"html/template"
"net/http"
"os"
"strings"
"time"
import "net/http"
"github.com/asdine/storm"
)
// StaticGen is a static website generator.
type StaticGen interface {
SettingsPath() string
Name() string
Setup() error
// RequestContext contains the needed information to make handlers work.
type RequestContext struct {
Hook(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
}
// Context contains the needed information to make handlers work.
type Context struct {
*FileManager
User *User
File *file
File *File
// On API handlers, Router is the APi handler we want.
Router string
}
// serveHTTP is the main entry point of this HTML application.
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
// returns a 404 error because we're not supposed to be here!
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
return http.StatusNotFound, nil
}
r.URL.Path = p
// Check if this request is made to the service worker. If so,
// pass it through a template to add the needed variables.
if r.URL.Path == "/sw.js" {
return renderFile(
c, w,
c.assets.MustString("sw.js"),
"application/javascript",
)
}
// Checks if this request is made to the static assets folder. If so, and
// if it is a GET request, returns with the asset. Otherwise, returns
// a status not implemented.
if matchURL(r.URL.Path, "/static") {
if r.Method != http.MethodGet {
return http.StatusNotImplemented, nil
}
return staticHandler(c, w, r)
}
// Checks if this request is made to the API and directs to the
// API handler if so.
if matchURL(r.URL.Path, "/api") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
return apiHandler(c, w, r)
}
// If it is a request to the preview and a static website generator is
// active, build the preview.
if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview")
return c.StaticGen.Preview(c, w, r)
}
if strings.HasPrefix(r.URL.Path, "/share/") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
return sharePage(c, w, r)
}
// Any other request should show the index.html file.
w.Header().Set("x-frame-options", "SAMEORIGIN")
w.Header().Set("x-content-type", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block")
return renderFile(
c, w,
c.assets.MustString("index.html"),
"text/html",
)
}
// staticHandler handles the static assets path.
func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil
}
return renderFile(
c, w,
c.assets.MustString("static/manifest.json"),
"application/json",
)
}
// apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth/get" {
return authHandler(c, w, r)
}
if r.URL.Path == "/auth/renew" {
return renewAuthHandler(c, w, r)
}
valid, _ := validateAuth(c, r)
if !valid {
return http.StatusForbidden, nil
}
c.Router, r.URL.Path = splitURL(r.URL.Path)
if !c.User.Allowed(r.URL.Path) {
return http.StatusForbidden, nil
}
if c.StaticGen != nil {
// If we are using the 'magic url' for the settings,
// we should redirect the request for the acutual path.
if r.URL.Path == "/settings" {
r.URL.Path = c.StaticGen.SettingsPath()
}
// Executes the Static website generator hook.
code, err := c.StaticGen.Hook(c, w, r)
if code != 0 || err != nil {
return code, err
}
}
if c.Router == "checksum" || c.Router == "download" {
var err error
c.File, err = getInfo(r.URL, c.FileManager, c.User)
if err != nil {
return errorToHTTP(err, false), err
}
}
var code int
var err error
switch c.Router {
case "download":
code, err = downloadHandler(c, w, r)
case "checksum":
code, err = checksumHandler(c, w, r)
case "command":
code, err = command(c, w, r)
case "search":
code, err = search(c, w, r)
case "resource":
code, err = resourceHandler(c, w, r)
case "users":
code, err = usersHandler(c, w, r)
case "settings":
code, err = settingsHandler(c, w, r)
case "share":
code, err = shareHandler(c, w, r)
default:
code = http.StatusNotFound
}
return code, err
}
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo")
val, err := c.File.Checksum(query)
if err == errInvalidOption {
return http.StatusBadRequest, err
} else if err != nil {
return http.StatusInternalServerError, err
}
w.Write([]byte(val))
return 0, nil
}
// splitURL splits the path and returns everything that stands
// before the first slash and everything that goes after.
func splitURL(path string) (string, string) {
if path == "" {
return "", ""
}
path = strings.TrimPrefix(path, "/")
i := strings.Index(path, "/")
if i == -1 {
return "", path
}
return path[0:i], path[i:]
}
// renderFile renders a file using a template with some needed variables.
func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentType string) (int, error) {
tpl := template.Must(template.New("file").Parse(file))
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(),
"StaticGen": c.staticgen,
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", r.URL.Path, &s)
if err == storm.ErrNotFound {
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
}
if err != nil {
return http.StatusInternalServerError, err
}
if s.Expires && s.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&s)
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
}
r.URL.Path = s.Path
info, err := os.Stat(s.Path)
if err != nil {
return errorToHTTP(err, false), err
}
c.File = &file{
Path: s.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
}
dl := r.URL.Query().Get("dl")
if dl == "" || dl == "0" {
tpl := template.Must(template.New("file").Parse(c.assets.MustString("static/share/index.html")))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(),
"File": c.File,
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
return downloadHandler(c, w, r)
}
// renderJSON prints the JSON version of data to the browser.
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
marsh, err := json.Marshal(data)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write(marsh); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// matchURL checks if the first URL matches the second.
func matchURL(first, second string) bool {
first = strings.ToLower(first)
second = strings.ToLower(second)
return strings.HasPrefix(first, second)
}
// errorToHTTP converts errors to HTTP Status Code.
func errorToHTTP(err error, gone bool) int {
switch {
case err == nil:
return http.StatusOK
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err):
if !gone {
return http.StatusNotFound
}
return http.StatusGone
case os.IsExist(err):
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"crypto/rand"
@ -11,17 +11,18 @@ import (
jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
fm "github.com/hacdias/filemanager"
)
// authHandler proccesses the authentication for the user.
func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// NoAuth instances shouldn't call this method.
if c.NoAuth {
return 0, nil
}
// Receive the credentials from the request and unmarshal them.
var cred User
var cred fm.User
if r.Body == nil {
return http.StatusForbidden, nil
}
@ -48,7 +49,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
// renewAuthHandler is used when the front-end already has a JWT token
// and is checking if it is up to date. If so, updates its info.
func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func renewAuthHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
ok, u := validateAuth(c, r)
if !ok {
return http.StatusForbidden, nil
@ -60,16 +61,16 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// claims is the JWT claims.
type claims struct {
User
fm.User
NoAuth bool `json:"noAuth"`
jwt.StandardClaims
}
// printToken prints the final JWT token to the user.
func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
func printToken(c *fm.Context, w http.ResponseWriter) (int, error) {
// Creates a copy of the user and removes it password
// hash so it never arrives to the user.
u := User{}
u := fm.User{}
u = *c.User
u.Password = ""
@ -119,7 +120,7 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
// validateAuth is used to validate the authentication and returns the
// User if it is valid.
func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
func validateAuth(c *fm.Context, r *http.Request) (bool, *fm.User) {
if c.NoAuth {
c.User = c.DefaultUser
return true, c.User

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"net/http"

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"io"
@ -9,13 +9,14 @@ import (
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
)
// downloadHandler creates an archive in one of the supported formats (zip, tar,
// tar.gz or tar.bz2) and sends it to be downloaded.
func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func downloadHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("format")
// If the file isn't a directory, serve it using http.ServeFile. We display it

324
http/http.go Normal file
View File

@ -0,0 +1,324 @@
package http
import (
"encoding/json"
"errors"
"html/template"
"net/http"
"os"
"strings"
"time"
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
var (
errUserExist = errors.New("user already exists")
errUserNotExist = errors.New("user does not exist")
errEmptyRequest = errors.New("request body is empty")
errEmptyPassword = errors.New("password is empty")
errEmptyUsername = errors.New("username is empty")
errEmptyScope = errors.New("scope is empty")
errWrongDataType = errors.New("wrong data type")
errInvalidUpdateField = errors.New("invalid field to update")
)
// ServeHTTP is the main entry point of this HTML application.
func ServeHTTP(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
// returns a 404 error because we're not supposed to be here!
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
return http.StatusNotFound, nil
}
r.URL.Path = p
// Check if this request is made to the service worker. If so,
// pass it through a template to add the needed variables.
if r.URL.Path == "/sw.js" {
return renderFile(
c, w,
c.assets.MustString("sw.js"),
"application/javascript",
)
}
// Checks if this request is made to the static assets folder. If so, and
// if it is a GET request, returns with the asset. Otherwise, returns
// a status not implemented.
if matchURL(r.URL.Path, "/static") {
if r.Method != http.MethodGet {
return http.StatusNotImplemented, nil
}
return staticHandler(c, w, r)
}
// Checks if this request is made to the API and directs to the
// API handler if so.
if matchURL(r.URL.Path, "/api") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
return apiHandler(c, w, r)
}
// If it is a request to the preview and a static website generator is
// active, build the preview.
if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview")
return c.StaticGen.Preview(c, w, r)
}
if strings.HasPrefix(r.URL.Path, "/share/") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
return sharePage(c, w, r)
}
// Any other request should show the index.html file.
w.Header().Set("x-frame-options", "SAMEORIGIN")
w.Header().Set("x-content-type", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block")
return renderFile(
c, w,
c.assets.MustString("index.html"),
"text/html",
)
}
// staticHandler handles the static assets path.
func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil
}
return renderFile(
c, w,
c.assets.MustString("static/manifest.json"),
"application/json",
)
}
// apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth/get" {
return authHandler(c, w, r)
}
if r.URL.Path == "/auth/renew" {
return renewAuthHandler(c, w, r)
}
valid, _ := validateAuth(c, r)
if !valid {
return http.StatusForbidden, nil
}
c.Router, r.URL.Path = splitURL(r.URL.Path)
if !c.User.Allowed(r.URL.Path) {
return http.StatusForbidden, nil
}
if c.StaticGen != nil {
// If we are using the 'magic url' for the settings,
// we should redirect the request for the acutual path.
if r.URL.Path == "/settings" {
r.URL.Path = c.StaticGen.SettingsPath()
}
// Executes the Static website generator hook.
code, err := c.StaticGen.Hook(c, w, r)
if code != 0 || err != nil {
return code, err
}
}
if c.Router == "checksum" || c.Router == "download" {
var err error
c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil {
return errorToHTTP(err, false), err
}
}
var code int
var err error
switch c.Router {
case "download":
code, err = downloadHandler(c, w, r)
case "checksum":
code, err = checksumHandler(c, w, r)
case "command":
code, err = command(c, w, r)
case "search":
code, err = search(c, w, r)
case "resource":
code, err = resourceHandler(c, w, r)
case "users":
code, err = usersHandler(c, w, r)
case "settings":
code, err = settingsHandler(c, w, r)
case "share":
code, err = shareHandler(c, w, r)
default:
code = http.StatusNotFound
}
return code, err
}
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo")
val, err := c.File.Checksum(query)
if err == errInvalidOption {
return http.StatusBadRequest, err
} else if err != nil {
return http.StatusInternalServerError, err
}
w.Write([]byte(val))
return 0, nil
}
// splitURL splits the path and returns everything that stands
// before the first slash and everything that goes after.
func splitURL(path string) (string, string) {
if path == "" {
return "", ""
}
path = strings.TrimPrefix(path, "/")
i := strings.Index(path, "/")
if i == -1 {
return "", path
}
return path[0:i], path[i:]
}
// renderFile renders a file using a template with some needed variables.
func renderFile(c *fm.Context, w http.ResponseWriter, file string, contentType string) (int, error) {
tpl := template.Must(template.New("file").Parse(file))
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(),
"StaticGen": c.staticgen,
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", r.URL.Path, &s)
if err == storm.ErrNotFound {
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
}
if err != nil {
return http.StatusInternalServerError, err
}
if s.Expires && s.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&s)
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
}
r.URL.Path = s.Path
info, err := os.Stat(s.Path)
if err != nil {
return errorToHTTP(err, false), err
}
c.File = &file{
Path: s.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
}
dl := r.URL.Query().Get("dl")
if dl == "" || dl == "0" {
tpl := template.Must(template.New("file").Parse(c.assets.MustString("static/share/index.html")))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(),
"File": c.File,
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
return downloadHandler(c, w, r)
}
// renderJSON prints the JSON version of data to the browser.
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
marsh, err := json.Marshal(data)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write(marsh); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// matchURL checks if the first URL matches the second.
func matchURL(first, second string) bool {
first = strings.ToLower(first)
second = strings.ToLower(second)
return strings.HasPrefix(first, second)
}
// errorToHTTP converts errors to HTTP Status Code.
func errorToHTTP(err error, gone bool) int {
switch {
case err == nil:
return http.StatusOK
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err):
if !gone {
return http.StatusNotFound
}
return http.StatusGone
case os.IsExist(err):
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"errors"
@ -13,6 +13,7 @@ import (
"strings"
"time"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
)
@ -26,7 +27,7 @@ func sanitizeURL(url string) string {
return path
}
func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
@ -61,7 +62,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusNotImplemented, nil
}
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file.
f, err := getInfo(r.URL, c.FileManager, c.User)
if err != nil {
@ -103,7 +104,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return renderJSON(w, f)
}
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.File
f.Kind = "listing"
@ -134,7 +135,7 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
return renderJSON(w, f)
}
func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Prevent the removal of the root directory.
if r.URL.Path == "/" || !c.User.AllowEdit {
return http.StatusForbidden, nil
@ -149,7 +150,7 @@ func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Req
return http.StatusOK, nil
}
func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowNew && r.Method == http.MethodPost {
return http.StatusForbidden, nil
}
@ -220,7 +221,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
return http.StatusOK, nil
}
func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
publish := r.Header.Get("Publish")
schedule := r.Header.Get("Schedule")
@ -251,7 +252,7 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
return http.StatusOK, nil
}
func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// Before save command handler.
@ -273,7 +274,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit {
return http.StatusForbidden, nil
}

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"bytes"
@ -6,6 +6,7 @@ import (
"net/http"
"reflect"
fm "github.com/hacdias/filemanager"
"github.com/mitchellh/mapstructure"
)
@ -44,7 +45,7 @@ func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
return mod, nil
}
func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func settingsHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "" && r.URL.Path != "/" {
return http.StatusNotFound, nil
}
@ -64,7 +65,7 @@ type settingsGetRequest struct {
StaticGen []option `json:"staticGen"`
}
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func settingsGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
@ -93,7 +94,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return renderJSON(w, result)
}
func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func settingsPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"encoding/hex"
@ -10,6 +10,7 @@ import (
"github.com/asdine/storm"
"github.com/asdine/storm/q"
fm "github.com/hacdias/filemanager"
)
type shareLink struct {
@ -19,7 +20,7 @@ type shareLink struct {
ExpireDate time.Time `json:"expireDate"`
}
func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func shareHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
@ -34,7 +35,7 @@ func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
return http.StatusNotImplemented, nil
}
func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
var (
s []*shareLink
path = filepath.Join(string(c.User.FileSystem), r.URL.Path)
@ -59,7 +60,7 @@ func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return renderJSON(w, s)
}
func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
var s shareLink
@ -116,7 +117,7 @@ func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return renderJSON(w, s)
}
func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"encoding/json"
@ -10,6 +10,7 @@ import (
"strings"
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
type modifyRequest struct {
@ -19,12 +20,12 @@ type modifyRequest struct {
type modifyUserRequest struct {
*modifyRequest
Data *User `json:"data"`
Data *fm.User `json:"data"`
}
// usersHandler is the entry point of the users API. It's just a router
// to send the request to its
func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If the user isn't admin and isn't making a PUT
// request, then return forbidden.
if !c.User.Admin && r.Method != http.MethodPut {
@ -64,7 +65,7 @@ func getUserID(r *http.Request) (int, error) {
// getUser returns the user which is present in the request
// body. If the body is empty or the JSON is invalid, it
// returns an error.
func getUser(r *http.Request) (*User, string, error) {
func getUser(r *http.Request) (*fm.User, string, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, "", errEmptyRequest
@ -85,7 +86,7 @@ func getUser(r *http.Request) (*User, string, error) {
return mod.Data, mod.Which, nil
}
func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Request for the default user data.
if r.URL.Path == "/base" {
return renderJSON(w, c.DefaultUser)
@ -131,7 +132,7 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusNotFound, errUserNotExist
}
func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/" {
return http.StatusMethodNotAllowed, nil
}
@ -231,7 +232,7 @@ func checkFS(path string) (int, error) {
return 0, nil
}
func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil
}
@ -262,7 +263,7 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusOK, nil
}
func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// New users should be created on /api/users.
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil

View File

@ -1,4 +1,4 @@
package filemanager
package http
import (
"bytes"
@ -13,6 +13,7 @@ import (
"time"
"github.com/gorilla/websocket"
fm "github.com/hacdias/filemanager"
)
var upgrader = websocket.Upgrader{
@ -26,7 +27,7 @@ var (
)
// command handles the requests for VCS related commands: git, svn and mercurial
func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for errors.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
@ -239,7 +240,7 @@ func parseSearch(value string) *searchOptions {
}
// search searches for a file or directory.
func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for errors.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {

9156
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
package filemanager
package staticgen
import (
"errors"
@ -10,6 +10,7 @@ import (
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/varutils"
)
@ -17,15 +18,6 @@ var (
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
)
// StaticGen is a static website generator.
type StaticGen interface {
SettingsPath() string
Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
}
// Hugo is the Hugo static website generator.
type Hugo struct {
// Website root
@ -66,8 +58,13 @@ func (h Hugo) SettingsPath() string {
return "/config." + frontmatter
}
// Name is the plugin's name.
func (h Hugo) Name() string {
return "hugo"
}
// Hook is the pre-api handler.
func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func (h Hugo) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method.
if r.Method != http.MethodPost {
@ -110,7 +107,7 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
}
// Publish publishes a post.
func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// We only run undraft command if it is a file.
@ -127,7 +124,7 @@ func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Preview handles the preview path.
func (h *Hugo) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if h.previewPath == "" {
path, err := ioutil.TempDir("", "")
@ -186,7 +183,8 @@ func (h Hugo) undraft(file string) error {
return nil
}
func (h *Hugo) find() error {
// Setup sets up the plugin.
func (h *Hugo) Setup() error {
var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil {
return err
@ -194,114 +192,3 @@ func (h *Hugo) find() error {
return nil
}
// Jekyll is the Jekyll static website generator.
type Jekyll struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Jekyll executable path
Exe string `name:"Executable"`
// Jekyll arguments
Args []string `name:"Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// SettingsPath retrieves the correct settings path.
func (j Jekyll) SettingsPath() string {
return "/_config.yml"
}
// Hook is the pre-api handler.
func (j Jekyll) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
// Publish publishes a post.
func (j Jekyll) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// We only run undraft command if it is a file.
if err := j.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
// Regenerates the file
j.run()
return 0, nil
}
// Preview handles the preview path.
func (j *Jekyll) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if j.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
j.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := j.Args
args = append(args, "--baseurl", c.RootURL()+"/preview/")
args = append(args, "--drafts")
args = append(args, "--destination", j.previewPath)
// Builds the preview.
if err := runCommand(j.Exe, args, j.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
return 0, nil
}
func (j Jekyll) run() {
// If the CleanPublic option is enabled, clean it.
if j.CleanPublic {
os.RemoveAll(j.Public)
}
if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
log.Println(err)
}
}
func (j Jekyll) undraft(file string) error {
if !strings.Contains(file, "_drafts") {
return nil
}
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
}
func (j *Jekyll) find() error {
var err error
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
return err
}
return nil
}
// runCommand executes an external command
func runCommand(command string, args []string, path string) error {
cmd := exec.Command(command, args...)
cmd.Dir = path
out, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(out))
}
return nil
}

120
staticgen/jekyll.go Normal file
View File

@ -0,0 +1,120 @@
package staticgen
import (
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
)
// Jekyll is the Jekyll static website generator.
type Jekyll struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Jekyll executable path
Exe string `name:"Executable"`
// Jekyll arguments
Args []string `name:"Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// SettingsPath retrieves the correct settings path.
func (j Jekyll) SettingsPath() string {
return "/_config.yml"
}
// Hook is the pre-api handler.
func (j Jekyll) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
// Publish publishes a post.
func (j Jekyll) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// We only run undraft command if it is a file.
if err := j.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
// Regenerates the file
j.run()
return 0, nil
}
// Preview handles the preview path.
func (j *Jekyll) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if j.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
j.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := j.Args
args = append(args, "--baseurl", c.RootURL()+"/preview/")
args = append(args, "--drafts")
args = append(args, "--destination", j.previewPath)
// Builds the preview.
if err := runCommand(j.Exe, args, j.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
return 0, nil
}
func (j Jekyll) run() {
// If the CleanPublic option is enabled, clean it.
if j.CleanPublic {
os.RemoveAll(j.Public)
}
if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
log.Println(err)
}
}
func (j Jekyll) undraft(file string) error {
if !strings.Contains(file, "_drafts") {
return nil
}
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
}
// Setup sets up the plugin.
func (j *Jekyll) Setup() error {
var err error
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
return err
}
if len(j.Args) == 0 {
j.Args = []string{"build"}
}
if j.Args[0] != "build" {
j.Args = append([]string{"build"}, j.Args...)
}
return nil
}

19
staticgen/staticgen.go Normal file
View File

@ -0,0 +1,19 @@
package staticgen
import (
"errors"
"os/exec"
)
// runCommand executes an external command
func runCommand(command string, args []string, path string) error {
cmd := exec.Command(command, args...)
cmd.Dir = path
out, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(out))
}
return nil
}

128
user.go Normal file
View File

@ -0,0 +1,128 @@
package filemanager
import (
"regexp"
"strings"
"github.com/hacdias/fileutils"
)
// DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
FileSystem: fileutils.Dir("."),
}
// User contains the configuration for each user.
type User struct {
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// Username is the user username used to login.
Username string `json:"username" storm:"index,unique"`
// The hashed password. This never reaches the front-end because it's temporarily
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// FileSystem is the virtual file system the user has access.
FileSystem fileutils.Dir `json:"filesystem"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
}
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule
i := len(u.Rules) - 1
for i >= 0 {
rule = u.Rules[i]
if rule.Regex {
if rule.Regexp.MatchString(url) {
return rule.Allow
}
} else if strings.HasPrefix(url, rule.Path) {
return rule.Allow
}
i--
}
return true
}
// MatchString checks if this string matches the regular expression.
func (r *Regexp) MatchString(s string) bool {
if r.regexp == nil {
r.regexp = regexp.MustCompile(r.Raw)
}
return r.regexp.MatchString(s)
}
type UsersStore interface {
Get(id int) (*User, error)
Gets() ([]*User, error)
Save(u *User, fields ...string) error
Delete(id int) error
}
type ConfigStore interface {
Get(name string, to interface{}) error
Save(name string, from interface{}) error
}
type ShareStore interface {
Get(hash string)
Save()
}