Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c8b118848 | ||
|
|
146744ea93 | ||
|
|
689077c545 | ||
|
|
53bba54ce4 | ||
|
|
d8974ce3af | ||
|
|
8d715bb433 | ||
|
|
25a86a9382 | ||
|
|
97f31310c6 | ||
|
|
97b5cea521 | ||
|
|
2462346e56 | ||
|
|
aaf6d60c3c | ||
|
|
3ff8908047 | ||
|
|
d5e943069e | ||
|
|
30be924648 | ||
|
|
bfdb924cb7 | ||
|
|
6d853d63ed | ||
|
|
4bae90c80f | ||
|
|
e464eb69ed | ||
|
|
fee9330cfa | ||
|
|
b034c87ec0 | ||
|
|
87420bcefd | ||
|
|
d6ca579519 | ||
|
|
d74c9ae5af | ||
|
|
2f60562143 | ||
|
|
1527ca0c50 | ||
|
|
0dee98b40a | ||
|
|
412ac9c9d6 | ||
|
|
9c2609995a | ||
|
|
68cb4ee980 | ||
|
|
adc82dd85e | ||
|
|
4f3375ee8d | ||
|
|
bda4fc16eb | ||
|
|
aa219e701e | ||
|
|
2460fd4fae | ||
|
|
d50bec8caa | ||
|
|
a5a68a8944 | ||
|
|
2029ca9aa6 | ||
|
|
311b943e40 | ||
|
|
620344bf0e | ||
|
|
7bb2d37661 | ||
|
|
e5f2331fb7 | ||
|
|
f158b79f89 | ||
|
|
de9cf466c7 | ||
|
|
b2f6f95916 | ||
|
|
ea2cc50a3f | ||
|
|
ef61957127 | ||
|
|
efe36312ec | ||
|
|
c5fbb47147 | ||
|
|
6602735030 | ||
|
|
eaafbb5dba | ||
|
|
33a99cf60c | ||
|
|
0a4e119bc5 | ||
|
|
16c8b86241 | ||
|
|
49c8dfa12e | ||
|
|
5c2fda6b4a | ||
|
|
1d3be7cf60 | ||
|
|
b8daa19d8f | ||
|
|
c6b392f1fb | ||
|
|
75e4afc1cb | ||
|
|
3c9762ee97 | ||
|
|
5968111f3e | ||
|
|
00be85db13 | ||
|
|
0d453229d9 | ||
|
|
4a4db4f4ee | ||
|
|
1755d52019 | ||
|
|
ea72ff6990 | ||
|
|
8777050d3e | ||
|
|
ae860e5bfc | ||
|
|
7ce9c72dc5 | ||
|
|
37282b0721 | ||
|
|
22fbc7d02d | ||
|
|
4905fc3800 | ||
|
|
242b868a56 | ||
|
|
e3c1e11b88 | ||
|
|
3cee6b67f0 | ||
|
|
7f46ef9a97 | ||
|
|
25c20aaeb2 | ||
|
|
4c665ec69c | ||
|
|
75ed779b17 | ||
|
|
6c360ffc5c | ||
|
|
f40731d3b4 | ||
|
|
afb10cc02c | ||
|
|
5d2e8864e5 | ||
|
|
d82f198b48 | ||
|
|
bbbf2bac51 | ||
|
|
d87a8ee5ed | ||
|
|
a81cc1e4ca | ||
|
|
6c5111fd6e | ||
|
|
e846c16336 | ||
|
|
dde01c6931 | ||
|
|
8b40dc9d19 | ||
|
|
63fb4c0c52 | ||
|
|
76047f4146 | ||
|
|
fc4c6ec246 | ||
|
|
f5e93825d8 | ||
|
|
73d74394c8 | ||
|
|
3469d8c6e8 | ||
|
|
be082c1075 | ||
|
|
f271b18d97 | ||
|
|
a728d0352e | ||
|
|
2096d7d048 |
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
assets/
|
||||
testdata/
|
||||
caddy/
|
||||
.github/
|
||||
29
.goreleaser.yml
Normal file
29
.goreleaser.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
build:
|
||||
main: cmd/filemanager/main.go
|
||||
binary: filemanager
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- netbsd
|
||||
- openbsd
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
|
||||
archive:
|
||||
name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}"
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
20
.travis.yml
20
.travis.yml
@@ -1,15 +1,12 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- tip
|
||||
go: 1.8.3
|
||||
|
||||
env:
|
||||
- "PATH=/home/travis/gopath/bin:$PATH"
|
||||
|
||||
install:
|
||||
- go get ./...
|
||||
- go get github.com/mitchellh/gox
|
||||
- go get github.com/tcnksm/ghr
|
||||
# Install gometalinter and certain linters
|
||||
- go get github.com/alecthomas/gometalinter
|
||||
- go get github.com/client9/misspell/cmd/misspell
|
||||
@@ -20,17 +17,6 @@ install:
|
||||
script:
|
||||
- gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --exclude="rice-box.go" --tests ./...
|
||||
- go test ./... -timeout 30s
|
||||
- cd cmd/filemanager
|
||||
- mkdir dist
|
||||
- gox -output "dist/{{.OS}}-{{.Arch}}-{{.Dir}}"
|
||||
- ls dist/
|
||||
- ghr --username hacdias --token $GITHUB_TOKEN --replace --prerelease --debug latest dist/
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: $GITHUB_TOKEN
|
||||
file_glob: true
|
||||
file: dist/*
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
after_success:
|
||||
- test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash
|
||||
|
||||
14
CONTRIBUTING.md
Normal file
14
CONTRIBUTING.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Contributing
|
||||
|
||||
If you want to contribute or want to build the code from source, you will need to have the most recent version of Go and, if you want to change the static assets (JS, CSS, ...), Node.js installed on your computer. To start developing, you just need to do the following:
|
||||
|
||||
1. `go get github.com/hacdias/filemanager`
|
||||
2. `cd $GOPATH/src/github.com/hacdias/filemanager`
|
||||
3. `npm install`
|
||||
4. `npm run dev` - regenerates the static assets automatically
|
||||
5. `go install github.com/hacdias/filemanager/cmd/filemanager`
|
||||
6. Execute `$GOPATH/bin/filemanager`
|
||||
|
||||
The steps 3 and 4 are only required **if you want to develop the front-end**. Otherwise, you can ignore them. Before pulling, if you made any change on assets folder, you must run the `build.sh` script on the root of this repository.
|
||||
|
||||
If you are using this as a Caddy plugin, you should use its [official instructions for plugins](https://github.com/mholt/caddy/wiki/Extending-Caddy#2-plug-in-your-plugin) and import `github.com/hacdias/filemanager/caddy/filemanager`.
|
||||
10
Docker.json
Normal file
10
Docker.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"port": 80,
|
||||
"address": "",
|
||||
"database": "/etc/database.db",
|
||||
"scope": "/srv",
|
||||
"allowCommands": true,
|
||||
"allowEdit": true,
|
||||
"allowNew": true,
|
||||
"commands": []
|
||||
}
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM golang:alpine
|
||||
|
||||
COPY . /go/src/github.com/hacdias/filemanager
|
||||
|
||||
WORKDIR /go/src/github.com/hacdias/filemanager
|
||||
RUN apk add --no-cache git
|
||||
RUN go get ./...
|
||||
|
||||
WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager
|
||||
RUN go build -ldflags "-X main.version=$(git tag -l --points-at HEAD)"
|
||||
RUN mv filemanager /go/bin/filemanager
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=0 /go/bin/filemanager /usr/local/bin/filemanager
|
||||
|
||||
VOLUME /srv
|
||||
EXPOSE 80
|
||||
|
||||
COPY Docker.json /etc/config.json
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/filemanager"]
|
||||
CMD ["--config", "/etc/config.json"]
|
||||
70
README.md
70
README.md
@@ -11,8 +11,6 @@ filemanager provides a file managing interface within a specified directory and
|
||||
# Table of contents
|
||||
|
||||
+ [Getting started](#getting-started)
|
||||
- [Caddy](#caddy)
|
||||
- [Standalone](#standalone)
|
||||
+ [Features](#features)
|
||||
- [Users](#users)
|
||||
- [Search](#search)
|
||||
@@ -21,61 +19,7 @@ filemanager provides a file managing interface within a specified directory and
|
||||
|
||||
# Getting started
|
||||
|
||||
This is a library that can be used on your own applications as a middleware (see the [documentation](http://godoc.org/github.com/hacdias/filemanager)), as a plugin to Caddy web server or as a standalone app.
|
||||
|
||||
Once you have everything deployed, the default credentials to login to the filemanager are:
|
||||
|
||||
**Username:** `admin`
|
||||
**Password:** `admin`
|
||||
|
||||
## Caddy
|
||||
|
||||
The easiest way to get started is using this with Caddy web server. You just need to download Caddy from its [official website](https://caddyserver.com/download) with `http.filemanager` plugin enabled. For more information about the plugin itself, please refer to its [documentation](https://caddyserver.com/docs/http.filemanager).
|
||||
|
||||
## Standalone
|
||||
|
||||
You can use filemanager as a standalone executable. You just need to download it from the [releases page](https://github.com/hacdias/filemanager/releases), where you can find multiple releases. The 'latest' always corresponds to the latest commit made to the master branch so it might not be stable.
|
||||
|
||||
You can either use flags or a JSON configuration file, which should have the following appearance:
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 80,
|
||||
"address": "127.0.0.1",
|
||||
"database": "/path/to/database.db",
|
||||
"scope": "/path/to/my/files",
|
||||
"allowCommands": true,
|
||||
"allowEdit": true,
|
||||
"allowNew": true,
|
||||
"commands": [
|
||||
"git",
|
||||
"svn"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `scope`, `allowCommands`, `allowEdit`, `allowNew` and `commands` options are the defaults for new users. To set a configuration file, you will need to pass the path with a flag, like this: `filemanager --config=/path/to/config.json`.
|
||||
|
||||
Otherwise, you may not want to use a configuration file, which can be done using the following flags:
|
||||
|
||||
```
|
||||
-address string
|
||||
Address to listen to (default is all of them)
|
||||
-allow-commands
|
||||
Default allow commands option (default true)
|
||||
-allow-edit
|
||||
Default allow edit option (default true)
|
||||
-allow-new
|
||||
Default allow new option (default true)
|
||||
-commands string
|
||||
Space separated commands available for new users (default "git svn hg")
|
||||
-database string
|
||||
Database path (default "./filemanager.db")
|
||||
-port string
|
||||
HTTP Port (default is random)
|
||||
-scope string
|
||||
Default scope for new users (default ".")
|
||||
```
|
||||
You can find the Getting Started guide on the [documentation](https://henriquedias.com/filemanager/quick-start/).
|
||||
|
||||
# Features
|
||||
|
||||
@@ -127,17 +71,7 @@ this are keywords case:insensitive
|
||||
|
||||
# Contributing
|
||||
|
||||
If you want to contribute or want to build the code from source, you will need to have NodeJS and Go installed on your computer. You should start by doing the following:
|
||||
|
||||
```
|
||||
go get github.com/hacdias/filemanager
|
||||
```
|
||||
|
||||
Then, you should navigate to `$GOPATH/src/github.com/hacdias/filemanager` and execute `npm install`. You can start the live build of static assets with the command `npm start dev`.
|
||||
|
||||
If you are using this as a Caddy plugin, you should use its [official instructions for plugins](https://github.com/mholt/caddy/wiki/Extending-Caddy#2-plug-in-your-plugin) and import `github.com/hacdias/filemanager/caddy/filemanager`.
|
||||
|
||||
Before pulling, and if you made any change on assets folder, you must run the `build.sh` script on the root of this repository.
|
||||
The contributing guidelines can be found [here](https://github.com/hacdias/filemanager/blob/master/CONTRIBUTING.md).
|
||||
|
||||
# Donate
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ if (fs.existsSync('./rice-box.go')) {
|
||||
fs.unlinkSync('./rice-box.go')
|
||||
}
|
||||
|
||||
if (fs.existsSync('./caddy/hugo/rice-box.go')) {
|
||||
fs.unlinkSync('./caddy/hugo/rice-box.go')
|
||||
if (fs.existsSync('./plugins/rice-box.go')) {
|
||||
fs.unlinkSync('./plugins/rice-box.go')
|
||||
}
|
||||
|
||||
rm(path.join(config.assetsRoot, config.assetsSubDirectory), err => {
|
||||
|
||||
@@ -25,6 +25,10 @@ module.exports = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(yml|yaml)$/,
|
||||
loader: 'yml-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<meta name="base" content="{{ .BaseURL }}">
|
||||
<meta name="staticgen" content="{{ .StaticGen }}">
|
||||
<title>File Manager</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
|
||||
<!--[if IE]><link rel="shortcut icon" href="/static/img/icons/favicon.ico"><![endif]-->
|
||||
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
|
||||
<meta name="theme-color" content="#2979ff">
|
||||
@@ -21,14 +22,11 @@
|
||||
<!-- Add to home screen for Windows -->
|
||||
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
|
||||
<meta name="msapplication-TileColor" content="#2979ff">
|
||||
|
||||
<% for (var chunk of webpack.chunks) {
|
||||
<% for (var chunk of webpack.compilation.chunks) {
|
||||
for (var file of chunk.files) {
|
||||
if (file.match(/\.(js|css)$/)) { %>
|
||||
<link rel="<%= chunk.initial?'preload':'prefetch' %>" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
||||
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
||||
|
||||
<!-- Plugins info -->
|
||||
<script>{{ range $index, $plugin := .Plugins }}{{ JS $plugin.JavaScript }}{{ end}}</script>
|
||||
<style>
|
||||
#loading {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>Global Settings</h1>
|
||||
|
||||
<ul>
|
||||
<li><router-link to="/settings/profile">Go to Profile Settings</router-link></li>
|
||||
<li><router-link to="/users">Go to User Management</router-link></li>
|
||||
</ul>
|
||||
|
||||
<form @submit="savePlugin" v-if="plugins.length > 0">
|
||||
<template v-for="plugin in plugins">
|
||||
<h2>{{ capitalize(plugin.name) }}</h2>
|
||||
|
||||
<p v-for="field in plugin.fields" :key="field.name">
|
||||
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
|
||||
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
|
||||
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
|
||||
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<p><input type="submit" value="Save"></p>
|
||||
</form>
|
||||
|
||||
<form @submit="saveCommands">
|
||||
<h2>Commands</h2>
|
||||
|
||||
<p class="small">Here you can set commands that are executed in the named events. You write one command
|
||||
per line. If the event is related to files, such as before and after saving, the environment variable
|
||||
<code>file</code> will be available with the path of the file.</p>
|
||||
|
||||
<template v-for="command in commands">
|
||||
<h3>{{ capitalize(command.name) }}</h3>
|
||||
<textarea v-model.trim="command.value"></textarea>
|
||||
</template>
|
||||
|
||||
<p><input type="submit" value="Save"></p>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
data: function () {
|
||||
return {
|
||||
commands: [],
|
||||
plugins: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ])
|
||||
},
|
||||
created () {
|
||||
api.getCommands()
|
||||
.then(commands => {
|
||||
for (let key in commands) {
|
||||
this.commands.push({
|
||||
name: key,
|
||||
value: commands[key].join('\n')
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
|
||||
api.getPlugins()
|
||||
.then(plugins => {
|
||||
console.log(plugins)
|
||||
let plugin = {}
|
||||
|
||||
for (let key in plugins) {
|
||||
plugin.name = key
|
||||
plugin.fields = []
|
||||
|
||||
for (let field in plugins[key]) {
|
||||
let value = plugins[key][field]
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
plugin.fields.push({
|
||||
name: field,
|
||||
type: 'text',
|
||||
original: 'array',
|
||||
value: value.join(' ')
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
plugin.fields.push({
|
||||
name: field,
|
||||
type: 'checkbox',
|
||||
original: 'boolean',
|
||||
value: value
|
||||
})
|
||||
break
|
||||
default:
|
||||
plugin.fields.push({
|
||||
name: field,
|
||||
type: 'text',
|
||||
original: 'text',
|
||||
value: value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.plugins.push(plugin)
|
||||
}
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showSuccess', 'showError' ]),
|
||||
capitalize (name, where = '_') {
|
||||
if (where === 'caps') where = /(?=[A-Z])/
|
||||
let splitted = name.split(where)
|
||||
name = ''
|
||||
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
||||
}
|
||||
|
||||
return name.slice(0, -1)
|
||||
},
|
||||
saveCommands (event) {
|
||||
event.preventDefault()
|
||||
|
||||
let commands = {}
|
||||
|
||||
for (let command of this.commands) {
|
||||
let value = command.value.split('\n')
|
||||
if (value.length === 1 && value[0] === '') {
|
||||
value = []
|
||||
}
|
||||
|
||||
commands[command.name] = value
|
||||
}
|
||||
|
||||
api.updateCommands(commands)
|
||||
.then(() => { this.showSuccess('Commands updated!') })
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
savePlugin (event) {
|
||||
event.preventDefault()
|
||||
let plugins = {}
|
||||
|
||||
for (let plugin of this.plugins) {
|
||||
let p = {}
|
||||
|
||||
for (let field of plugin.fields) {
|
||||
p[field.name] = field.value
|
||||
|
||||
if (field.original === 'array') {
|
||||
let val = field.value.split(' ')
|
||||
if (val[0] === '') {
|
||||
val.shift()
|
||||
}
|
||||
|
||||
p[field.name] = val
|
||||
}
|
||||
}
|
||||
|
||||
plugins[plugin.name] = p
|
||||
}
|
||||
|
||||
console.log(plugins)
|
||||
|
||||
api.updatePlugins(plugins)
|
||||
.then(() => { this.showSuccess('Plugins settings updated!') })
|
||||
.catch(error => { this.showError(error) })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,42 +1,35 @@
|
||||
<template>
|
||||
<header>
|
||||
<div>
|
||||
<button @click="openSidebar" aria-label="Toggle sidebar" title="Toggle sidebar" class="action">
|
||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||
<i class="material-icons">menu</i>
|
||||
</button>
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<search></search>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="openSearch" aria-label="Search" title="Search" class="search-button action">
|
||||
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
||||
<i class="material-icons">search</i>
|
||||
</button>
|
||||
|
||||
<button v-show="showSaveButton" aria-label="Save" class="action" id="save-button">
|
||||
<i class="material-icons" title="Save">save</i>
|
||||
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
|
||||
<div v-for="plugin in plugins" :key="plugin.name">
|
||||
<button class="action"
|
||||
v-for="action in plugin.header.visible"
|
||||
v-if="action.if(pluginData, $route)"
|
||||
@click="action.click($event, pluginData, $route)"
|
||||
:aria-label="action.name"
|
||||
:id="action.id"
|
||||
:title="action.name"
|
||||
:key="action.name">
|
||||
<i class="material-icons">{{ action.icon }}</i>
|
||||
<span>{{ action.name }}</span>
|
||||
<template v-if="staticGen.length > 0">
|
||||
<button v-show="showPublishButton" :aria-label="$t('buttons.publish')" :title="$t('buttons.publish')" class="action" id="publish-button">
|
||||
<i class="material-icons">send</i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button @click="openMore" id="more" aria-label="More" title="More" class="action">
|
||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</button>
|
||||
|
||||
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
||||
<div id="file-selection" v-if="isMobile && req.kind === 'listing'">
|
||||
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
||||
<share-button v-show="showRenameButton"></share-button>
|
||||
<rename-button v-show="showRenameButton"></rename-button>
|
||||
<copy-button v-show="showMoveButton"></copy-button>
|
||||
<move-button v-show="showMoveButton"></move-button>
|
||||
@@ -46,34 +39,25 @@
|
||||
<!-- This buttons are shown on a dropdown on mobile phones -->
|
||||
<div id="dropdown" :class="{ active: showMore }">
|
||||
<div v-if="!isListing || !isMobile">
|
||||
<share-button v-show="showRenameButton"></share-button>
|
||||
<rename-button v-show="showRenameButton"></rename-button>
|
||||
<copy-button v-show="showMoveButton"></copy-button>
|
||||
<move-button v-show="showMoveButton"></move-button>
|
||||
<delete-button v-show="showDeleteButton"></delete-button>
|
||||
</div>
|
||||
|
||||
<div v-for="plugin in plugins" :key="plugin.name">
|
||||
<button class="action"
|
||||
v-for="action in plugin.header.hidden"
|
||||
v-if="action.if(pluginData, $route)"
|
||||
@click="action.click($event, pluginData, $route)"
|
||||
:id="action.id"
|
||||
:aria-label="action.name"
|
||||
:title="action.name"
|
||||
:key="action.name">
|
||||
<i class="material-icons">{{ action.icon }}</i>
|
||||
<span>{{ action.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="staticGen.length > 0">
|
||||
<schedule-button v-show="showPublishButton"></schedule-button>
|
||||
</template>
|
||||
|
||||
<switch-button v-show="showSwitchButton"></switch-button>
|
||||
<download-button v-show="showCommonButton"></download-button>
|
||||
<upload-button v-show="showUpload"></upload-button>
|
||||
<info-button v-show="showCommonButton"></info-button>
|
||||
|
||||
<button v-show="showSelectButton" @click="openSelect" aria-label="Select multiple" class="action">
|
||||
<button v-show="showSelectButton" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
|
||||
<i class="material-icons">check_circle</i>
|
||||
<span>Select</span>
|
||||
<span>{{ $t('buttons.select') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
@@ -91,8 +75,10 @@ import DownloadButton from './buttons/Download'
|
||||
import SwitchButton from './buttons/SwitchView'
|
||||
import MoveButton from './buttons/Move'
|
||||
import CopyButton from './buttons/Copy'
|
||||
import ScheduleButton from './buttons/Schedule'
|
||||
import ShareButton from './buttons/Share'
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
@@ -101,12 +87,14 @@ export default {
|
||||
Search,
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
ShareButton,
|
||||
RenameButton,
|
||||
DownloadButton,
|
||||
CopyButton,
|
||||
UploadButton,
|
||||
SwitchButton,
|
||||
MoveButton
|
||||
MoveButton,
|
||||
ScheduleButton
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
@@ -134,7 +122,7 @@ export default {
|
||||
'loading',
|
||||
'reload',
|
||||
'multiple',
|
||||
'plugins'
|
||||
'staticGen'
|
||||
]),
|
||||
isMobile () {
|
||||
return this.width <= 736
|
||||
@@ -148,6 +136,9 @@ export default {
|
||||
showSaveButton () {
|
||||
return (this.req.kind === 'editor' && !this.loading)
|
||||
},
|
||||
showPublishButton () {
|
||||
return (this.req.kind === 'editor' && !this.loading && this.user.allowPublish)
|
||||
},
|
||||
showSwitchButton () {
|
||||
return this.req.kind === 'listing' && this.$route.name === 'Files' && !this.loading
|
||||
},
|
||||
|
||||
20
assets/src/components/Languages.vue
Normal file
20
assets/src/components/Languages.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="selected">
|
||||
<option value="en">{{ $t('languages.en') }}</option>
|
||||
<option value="pt">{{ $t('languages.pt') }}</option>
|
||||
<option value="ja">{{ $t('languages.ja') }}</option>
|
||||
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'languages',
|
||||
props: [ 'selected' ],
|
||||
methods: {
|
||||
change (event) {
|
||||
this.$emit('update:selected', event.target.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,118 +0,0 @@
|
||||
<template>
|
||||
<div id="login">
|
||||
<form @submit="submit">
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<h1>File Manager</h1>
|
||||
<div v-if="wrong" class="wrong">Wrong credentials</div>
|
||||
<input type="text" v-model="username" placeholder="Username">
|
||||
<input type="password" v-model="password" placeholder="Password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
data: function () {
|
||||
return {
|
||||
wrong: false,
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
let redirect = this.$route.query.redirect
|
||||
if (redirect === '' || redirect === undefined || redirect === null) {
|
||||
redirect = '/files/'
|
||||
}
|
||||
|
||||
auth.login(this.username, this.password)
|
||||
.then(() => {
|
||||
this.$router.push({ path: redirect })
|
||||
})
|
||||
.catch(() => {
|
||||
this.wrong = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#login {
|
||||
background: #fff;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#login img {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#login h1 {
|
||||
text-align: center;
|
||||
font-size: 2.5em;
|
||||
margin: .4em 0 .67em;
|
||||
}
|
||||
|
||||
#login form {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 16em;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#login input {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
#login .wrong {
|
||||
background: #F44336;
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
}
|
||||
|
||||
@keyframes opac {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#login input[type="text"],
|
||||
#login input[type="password"] {
|
||||
padding: .5em 1em;
|
||||
border: 1px solid #e9e9e9;
|
||||
transition: .2s ease border;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#login input[type="text"]:focus,
|
||||
#login input[type="password"]:focus,
|
||||
#login input[type="text"]:hover,
|
||||
#login input[type="password"]:hover {
|
||||
border-color: #9f9f9f;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>Profile Settings</h1>
|
||||
|
||||
<ul v-if="user.admin">
|
||||
<li><router-link to="/settings/global">Go to Global Settings</router-link></li>
|
||||
</ul>
|
||||
|
||||
<form @submit="changePassword">
|
||||
<h2>Change Password</h2>
|
||||
<p><input :class="passwordClass" type="password" placeholder="Your new password" v-model="password" name="password"></p>
|
||||
<p><input :class="passwordClass" type="password" placeholder="Confirm your new password" v-model="passwordConf" name="password"></p>
|
||||
<p><input type="submit" value="Change Password"></p>
|
||||
</form>
|
||||
|
||||
<form @submit="updateCSS">
|
||||
<h2>Custom Stylesheet</h2>
|
||||
<textarea v-model="css" name="css"></textarea>
|
||||
<p><input type="submit" value="Update"></p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
data: function () {
|
||||
return {
|
||||
password: '',
|
||||
passwordConf: '',
|
||||
css: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ]),
|
||||
passwordClass () {
|
||||
if (this.password === '' && this.passwordConf === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (this.password === this.passwordConf) {
|
||||
return 'green'
|
||||
}
|
||||
|
||||
return 'red'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.css = this.user.css
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showSuccess' ]),
|
||||
changePassword (event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (this.password !== this.passwordConf) {
|
||||
return
|
||||
}
|
||||
|
||||
api.updatePassword(this.password).then(() => {
|
||||
this.showSuccess('Password updated!')
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
},
|
||||
updateCSS (event) {
|
||||
event.preventDefault()
|
||||
|
||||
api.updateCSS(this.css).then(() => {
|
||||
this.$store.commit('setUserCSS', this.css)
|
||||
this.$emit('css-updated')
|
||||
this.showSuccess('Styles updated!')
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
|
||||
<div id="input">
|
||||
<button v-if="active" class="action" @click="close">
|
||||
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</button>
|
||||
<i v-else class="material-icons">search</i>
|
||||
@@ -11,24 +11,70 @@
|
||||
ref="input"
|
||||
:autofocus="active"
|
||||
v-model.trim="value"
|
||||
aria-label="Write here to search"
|
||||
:aria-label="$t('search.writeToSearch')"
|
||||
:placeholder="placeholder">
|
||||
</div>
|
||||
|
||||
<div id="result">
|
||||
<div>
|
||||
<span v-if="search.length === 0 && commands.length === 0">{{ text }}</span>
|
||||
<template v-if="search.length === 0 && commands.length === 0">
|
||||
<p>{{ text }}</p>
|
||||
|
||||
<template v-if="value.length === 0">
|
||||
<div class="boxes">
|
||||
<h3>{{ $t('search.types') }}</h3>
|
||||
<div>
|
||||
<div tabindex="0"
|
||||
role="button"
|
||||
@click="init('type:image')"
|
||||
:aria-label="$t('search.images')">
|
||||
<i class="material-icons">insert_photo</i>
|
||||
<p>{{ $t('search.images') }}</p>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"
|
||||
role="button"
|
||||
@click="init('type:audio')"
|
||||
:aria-label="$t('search.music')">
|
||||
<i class="material-icons">volume_up</i>
|
||||
<p>{{ $t('search.music') }}</p>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"
|
||||
role="button"
|
||||
@click="init('type:video')"
|
||||
:aria-label="$t('search.video')">
|
||||
<i class="material-icons">movie</i>
|
||||
<p>{{ $t('search.video') }}</p>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"
|
||||
role="button"
|
||||
@click="init('type:pdf')"
|
||||
:aria-label="$t('search.pdf')">
|
||||
<i class="material-icons">picture_as_pdf</i>
|
||||
<p>{{ $t('search.pdf') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
<ul v-else-if="search.length > 0">
|
||||
<li v-for="s in search">
|
||||
<router-link @click.native="close" :to="'./' + s">./{{ s }}</router-link>
|
||||
<router-link @click.native="close" :to="'./' + s.path">
|
||||
<i v-if="s.dir" class="material-icons">folder</i>
|
||||
<i v-else class="material-icons">insert_drive_file</i>
|
||||
<span>./{{ s.path }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul v-else-if="commands.length > 0">
|
||||
<li v-for="c in commands">{{ c }}</li>
|
||||
</ul>
|
||||
<pre v-else-if="commands.length > 0">
|
||||
<template v-for="c in commands">{{ c }}</template>
|
||||
</pre>
|
||||
</div>
|
||||
<p><i class="material-icons spin">autorenew</i></p>
|
||||
<p id="renew"><i class="material-icons spin">autorenew</i></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,7 +82,7 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'search',
|
||||
@@ -47,7 +93,8 @@ export default {
|
||||
ongoing: false,
|
||||
scrollable: null,
|
||||
search: [],
|
||||
commands: []
|
||||
commands: [],
|
||||
reload: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -57,13 +104,21 @@ export default {
|
||||
// If the hover was search and now it's something else
|
||||
// we should blur the input.
|
||||
if (old === 'search' && val !== 'search') {
|
||||
if (this.reload) {
|
||||
this.$store.commit('setReload', true)
|
||||
}
|
||||
|
||||
document.body.style.overflow = 'auto'
|
||||
this.reset()
|
||||
this.$refs.input.blur()
|
||||
}
|
||||
|
||||
// If we are starting to show the search box, we should
|
||||
// focus the input.
|
||||
if (val === 'search') {
|
||||
this.reload = false
|
||||
this.$refs.input.focus()
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -72,10 +127,10 @@ export default {
|
||||
// Placeholder value.
|
||||
placeholder: function () {
|
||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||
return 'Search or execute a command...'
|
||||
return this.$t('search.searchOrCommand')
|
||||
}
|
||||
|
||||
return 'Search...'
|
||||
return this.$t('search.search')
|
||||
},
|
||||
// The text that is shown on the results' box while
|
||||
// there is no search result or command output to show.
|
||||
@@ -86,16 +141,16 @@ export default {
|
||||
|
||||
if (this.value.length === 0) {
|
||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||
return `Search or use one of your supported commands: ${this.user.commands.join(', ')}.`
|
||||
return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.`
|
||||
}
|
||||
|
||||
return 'Type and press enter to search.'
|
||||
this.$t('search.type')
|
||||
}
|
||||
|
||||
if (!this.supported() || !this.user.allowCommands) {
|
||||
return 'Press enter to search.'
|
||||
return this.$t('search.pressToSearch')
|
||||
} else {
|
||||
return 'Press enter to execute.'
|
||||
return this.$t('search.pressToExecute')
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -113,34 +168,47 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
// Sets the search to active.
|
||||
open: function (event) {
|
||||
open (event) {
|
||||
this.$store.commit('showHover', 'search')
|
||||
},
|
||||
// Closes the search and prevents the event
|
||||
// of propagating so it doesn't trigger the
|
||||
// click event on #search.
|
||||
close: function (event) {
|
||||
close (event) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
// Checks if the current input is a supported command.
|
||||
supported: function () {
|
||||
supported () {
|
||||
let pieces = this.value.split(' ')
|
||||
|
||||
for (let i = 0; i < this.user.commands.length; i++) {
|
||||
if (pieces[0] === this.user.commands[0]) {
|
||||
if (pieces[0] === this.user.commands[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
// Initializes the search with a default value.
|
||||
init (string) {
|
||||
this.value = string + ' '
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
// Resets the search box value.
|
||||
reset () {
|
||||
this.value = ''
|
||||
this.active = false
|
||||
this.ongoing = false
|
||||
this.search = []
|
||||
this.commands = []
|
||||
},
|
||||
// When the user presses a key, if it is ESC
|
||||
// then it will close the search box. Otherwise,
|
||||
// it will set the search box to active and clean
|
||||
// the search results, as well as commands'.
|
||||
keyup: function (event) {
|
||||
keyup (event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.close(event)
|
||||
return
|
||||
@@ -150,7 +218,7 @@ export default {
|
||||
this.commands.length = 0
|
||||
},
|
||||
// Submits the input to the server and sets ongoing to true.
|
||||
submit: function (event) {
|
||||
submit (event) {
|
||||
this.ongoing = true
|
||||
|
||||
let path = this.$route.path
|
||||
@@ -166,9 +234,9 @@ export default {
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
},
|
||||
(event) => {
|
||||
this.reload = true
|
||||
this.ongoing = false
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
this.$store.commit('setReload', true)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -178,10 +246,12 @@ export default {
|
||||
// In case of being a search.
|
||||
api.search(path, this.value,
|
||||
(event) => {
|
||||
let url = event.data
|
||||
if (url[0] === '/') url = url.substring(1)
|
||||
let response = JSON.parse(event.data)
|
||||
if (response.path[0] === '/') {
|
||||
response.path = response.path.substring(1)
|
||||
}
|
||||
|
||||
this.search.push(url)
|
||||
this.search.push(response)
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||
},
|
||||
(event) => {
|
||||
|
||||
@@ -1,45 +1,66 @@
|
||||
<template>
|
||||
<nav :class="{active}">
|
||||
<router-link class="action" to="/files/" aria-label="My Files" title="My Files">
|
||||
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
|
||||
<i class="material-icons">folder</i>
|
||||
<span>My Files</span>
|
||||
<span>{{ $t('sidebar.myFiles') }}</span>
|
||||
</router-link>
|
||||
|
||||
<div v-if="user.allowNew">
|
||||
<button @click="$store.commit('showHover', 'newDir')" aria-label="New directory" title="New directory" class="action">
|
||||
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>New folder</span>
|
||||
<span>{{ $t('sidebar.newFolder') }}</span>
|
||||
</button>
|
||||
|
||||
<button @click="$store.commit('showHover', 'newFile')" aria-label="New file" title="New file" class="action">
|
||||
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
|
||||
<i class="material-icons">note_add</i>
|
||||
<span>New file</span>
|
||||
<span>{{ $t('sidebar.newFile') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="plugin in plugins" :key="plugin.name">
|
||||
<button v-for="action in plugin.sidebar" @click="action.click($event, pluginData, $route)" :aria-label="action.name" :title="action.name" :key="action.name" class="action">
|
||||
<i class="material-icons">{{ action.icon }}</i>
|
||||
<span>{{ action.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<router-link class="action" to="/settings" aria-label="Settings" title="Settings">
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>Settings</span>
|
||||
<div v-if="staticGen.length > 0">
|
||||
<router-link to="/files/settings"
|
||||
:aria-label="$t('sidebar.siteSettings')"
|
||||
:title="$t('sidebar.siteSettings')"
|
||||
class="action">
|
||||
<i class="material-icons">settings</i>
|
||||
<span>{{ $t('sidebar.siteSettings') }}</span>
|
||||
</router-link>
|
||||
|
||||
<button @click="logout" class="action" id="logout" aria-label="Log out" title="Logout">
|
||||
<template v-if="staticGen === 'hugo'">
|
||||
<button class="action"
|
||||
:aria-label="$t('sidebar.hugoNew')"
|
||||
:title="$t('sidebar.hugoNew')"
|
||||
v-if="user.allowNew"
|
||||
@click="$store.commit('showHover', 'new-archetype')">
|
||||
<i class="material-icons">merge_type</i>
|
||||
<span>{{ $t('sidebar.hugoNew') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<button class="action"
|
||||
:aria-label="$t('sidebar.preview')"
|
||||
:title="$t('sidebar.preview')"
|
||||
@click="preview">
|
||||
<i class="material-icons">remove_red_eye</i>
|
||||
<span>{{ $t('sidebar.preview') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!$store.state.user.noAuth">
|
||||
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>{{ $t('sidebar.settings') }}</span>
|
||||
</router-link>
|
||||
|
||||
<button @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>Logout</span>
|
||||
<span>{{ $t('sidebar.logout') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="credits">
|
||||
<span>Served with <a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-filemanager">File Manager</a>.</span>
|
||||
<span v-for="plugin in plugins" :key="plugin.name" v-html="plugin.credits"><br></span>
|
||||
<span><a @click="help">Help</a></span>
|
||||
<span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
|
||||
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
|
||||
</p>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -47,31 +68,22 @@
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import auth from '@/utils/auth'
|
||||
import buttons from '@/utils/buttons'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'sidebar',
|
||||
data: function () {
|
||||
return {
|
||||
pluginData: {
|
||||
api,
|
||||
buttons,
|
||||
'store': this.$store,
|
||||
'router': this.$router
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['user', 'plugins']),
|
||||
...mapState(['user', 'staticGen']),
|
||||
active () {
|
||||
return this.$store.state.show === 'sidebar'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
help: function () {
|
||||
help () {
|
||||
this.$store.commit('showHover', 'help')
|
||||
},
|
||||
preview () {
|
||||
window.open(this.$store.state.baseURL + '/preview/')
|
||||
},
|
||||
logout: auth.logout
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Copy" title="Copy" class="action" id="copy-button">
|
||||
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
|
||||
<i class="material-icons">content_copy</i>
|
||||
<span>Copy file</span>
|
||||
<span>{{ $t('buttons.copyFile') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Delete" title="Delete" class="action" id="delete-button">
|
||||
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
|
||||
<i class="material-icons">delete</i>
|
||||
<span>Delete</span>
|
||||
<span>{{ $t('buttons.delete') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<button @click="download" aria-label="Download" title="Download" id="download-button" class="action">
|
||||
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
|
||||
<i class="material-icons">file_download</i>
|
||||
<span>Download</span>
|
||||
<span>{{ $t('buttons.download') }}</span>
|
||||
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'download-button',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button title="Info" aria-label="Info" class="action" @click="show">
|
||||
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
|
||||
<i class="material-icons">info</i>
|
||||
<span>Info</span>
|
||||
<span>{{ $t('buttons.info') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Move" title="Move" class="action" id="move-button">
|
||||
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
|
||||
<i class="material-icons">forward</i>
|
||||
<span>Move file</span>
|
||||
<span>{{ $t('buttons.moveFile') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Rename" title="Rename" class="action" id="rename-button">
|
||||
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
|
||||
<i class="material-icons">mode_edit</i>
|
||||
<span>Rename</span>
|
||||
<span>{{ $t('buttons.rename') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
21
assets/src/components/buttons/Schedule.vue
Normal file
21
assets/src/components/buttons/Schedule.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<button @click="show"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')"
|
||||
id="schedule-button"
|
||||
class="action">
|
||||
<i class="material-icons">alarm</i>
|
||||
<span>{{ $t('buttons.schedule') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'schedule-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'schedule')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
17
assets/src/components/buttons/Share.vue
Normal file
17
assets/src/components/buttons/Share.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action">
|
||||
<i class="material-icons">share</i>
|
||||
<span>{{ $t('buttons.share') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'share-button',
|
||||
methods: {
|
||||
show (event) {
|
||||
this.$store.commit('showHover', 'share')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="change" aria-label="Switch View" title="Switch View" class="action" id="switch-view-button">
|
||||
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
|
||||
<i class="material-icons">{{ icon() }}</i>
|
||||
<span>Switch view</span>
|
||||
<span>{{ $t('buttons.switchView') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="upload" aria-label="Upload" title="Upload" class="action" id="upload-button">
|
||||
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
|
||||
<i class="material-icons">file_upload</i>
|
||||
<span>Upload</span>
|
||||
<span>{{ $t('buttons.upload') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<form id="editor" :class="req.language">
|
||||
<div v-if="hasMetadata" id="metadata">
|
||||
<h2>Metadata</h2>
|
||||
<h2>{{ $t('files.metadata') }}</h2>
|
||||
</div>
|
||||
|
||||
<h2 v-if="hasMetadata">Body</h2>
|
||||
<h2 v-if="hasMetadata">{{ $t('files.body') }}</h2>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import CodeMirror from '@/utils/codemirror'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
name: 'editor',
|
||||
computed: {
|
||||
...mapState(['req']),
|
||||
...mapState(['req', 'schedule']),
|
||||
hasMetadata: function () {
|
||||
return (this.req.metadata !== undefined && this.req.metadata !== null)
|
||||
}
|
||||
@@ -32,10 +32,20 @@ export default {
|
||||
created () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').addEventListener('click', this.save)
|
||||
|
||||
let publish = document.getElementById('publish-button')
|
||||
if (publish !== null) {
|
||||
publish.addEventListener('click', this.publish)
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').removeEventListener('click', this.save)
|
||||
|
||||
let publish = document.getElementById('publish-button')
|
||||
if (publish !== null) {
|
||||
publish.removeEventListener('click', this.publish)
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
if (this.req.content === undefined || this.req.content === null) {
|
||||
@@ -102,28 +112,32 @@ export default {
|
||||
this.metalang = 'toml'
|
||||
}
|
||||
},
|
||||
// Publishes the file.
|
||||
publish (event) {
|
||||
this.save(event, true)
|
||||
},
|
||||
// Saves the file.
|
||||
save () {
|
||||
buttons.loading('save')
|
||||
save (event, regenerate = false) {
|
||||
let button = regenerate ? 'publish' : 'save'
|
||||
if (this.schedule !== '') button = 'schedule'
|
||||
let content = this.content.getValue()
|
||||
buttons.loading(button)
|
||||
|
||||
if (this.hasMetadata) {
|
||||
content = this.metadata.getValue() + '\n\n' + content
|
||||
}
|
||||
|
||||
api.put(this.$route.path, content)
|
||||
api.put(this.$route.path, content, regenerate, this.schedule)
|
||||
.then(() => {
|
||||
buttons.done('save')
|
||||
buttons.success(button)
|
||||
this.$store.commit('setSchedule', '')
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('save')
|
||||
buttons.done(button)
|
||||
this.$store.commit('showError', error)
|
||||
this.$store.commit('setSchedule', '')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -2,35 +2,51 @@
|
||||
<div v-if="(req.numDirs + req.numFiles) == 0">
|
||||
<h2 class="message">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>It feels lonely here...</span>
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
</h2>
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="req.display"
|
||||
@drop="drop"
|
||||
@dragenter="dragEnter"
|
||||
@dragend="dragEnd">
|
||||
<div>
|
||||
<div class="item header">
|
||||
<div></div>
|
||||
<div>
|
||||
<p :class="{ active: nameSorted }" class="name" @click="sort('name')">
|
||||
<span>Name</span>
|
||||
<p :class="{ active: nameSorted }" class="name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('name')"
|
||||
:title="$t('files.sortByName')"
|
||||
:aria-label="$t('files.sortByName')">
|
||||
<span>{{ $t('files.name') }}</span>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p :class="{ active: !nameSorted }" class="size" @click="sort('size')">
|
||||
<span>Size</span>
|
||||
<p :class="{ active: sizeSorted }" class="size"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('size')"
|
||||
:title="$t('files.sortBySize')"
|
||||
:aria-label="$t('files.sortBySize')">
|
||||
<span>{{ $t('files.size') }}</span>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p class="modified">Last modified</p>
|
||||
<p :class="{ active: modifiedSorted }" class="modified"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')"
|
||||
:aria-label="$t('files.sortByLastModified')">
|
||||
<span>{{ $t('files.lastModified') }}</span>
|
||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numDirs > 0">Folders</h2>
|
||||
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
|
||||
<div v-if="req.numDirs > 0">
|
||||
<item v-for="(item, index) in req.items"
|
||||
v-if="item.isDir"
|
||||
@@ -45,7 +61,7 @@
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numFiles > 0">Files</h2>
|
||||
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
|
||||
<div v-if="req.numFiles > 0">
|
||||
<item v-for="(item, index) in req.items"
|
||||
v-if="!item.isDir"
|
||||
@@ -60,12 +76,12 @@
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
|
||||
<div v-show="$store.state.multiple" :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||
<p>Multiple selection enabled</p>
|
||||
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" title="Clear" aria-label="Clear" class="action">
|
||||
<i class="material-icons" title="Clear">clear</i>
|
||||
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
|
||||
<i class="material-icons">clear</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +91,7 @@
|
||||
import {mapState} from 'vuex'
|
||||
import Item from './ListingItem'
|
||||
import css from '@/utils/css'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
@@ -86,6 +102,12 @@ export default {
|
||||
nameSorted () {
|
||||
return (this.req.sort === 'name')
|
||||
},
|
||||
sizeSorted () {
|
||||
return (this.req.sort === 'size')
|
||||
},
|
||||
modifiedSorted () {
|
||||
return (this.req.sort === 'modified')
|
||||
},
|
||||
ascOrdered () {
|
||||
return (this.req.order === 'asc')
|
||||
},
|
||||
@@ -97,7 +119,14 @@ export default {
|
||||
return 'arrow_downward'
|
||||
},
|
||||
sizeIcon () {
|
||||
if (!this.nameSorted && this.ascOrdered) {
|
||||
if (this.sizeSorted && this.ascOrdered) {
|
||||
return 'arrow_downward'
|
||||
}
|
||||
|
||||
return 'arrow_upward'
|
||||
},
|
||||
modifiedIcon () {
|
||||
if (this.modifiedSorted && this.ascOrdered) {
|
||||
return 'arrow_downward'
|
||||
}
|
||||
|
||||
@@ -200,7 +229,7 @@ export default {
|
||||
if (columns === 0) columns = 1
|
||||
items.style.width = `calc(${100 / columns}% - 1em)`
|
||||
},
|
||||
dragEnter: function (event) {
|
||||
dragEnter (event) {
|
||||
// When the user starts dragging an item, put every
|
||||
// file on the listing with 50% opacity.
|
||||
let items = document.getElementsByClassName('item')
|
||||
@@ -209,56 +238,94 @@ export default {
|
||||
file.style.opacity = 0.5
|
||||
})
|
||||
},
|
||||
dragEnd: function (event) {
|
||||
dragEnd (event) {
|
||||
this.resetOpacity()
|
||||
},
|
||||
drop: function (event) {
|
||||
event.preventDefault()
|
||||
this.resetOpacity()
|
||||
|
||||
let dt = event.dataTransfer
|
||||
let files = dt.files
|
||||
let el = event.target
|
||||
|
||||
if (files.length <= 0) return
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains('item')) {
|
||||
el = el.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
|
||||
this.handleFiles(files, el.querySelector('.name').innerHTML + '/')
|
||||
return
|
||||
}
|
||||
|
||||
this.handleFiles(files, '')
|
||||
} else {
|
||||
this.resetOpacity()
|
||||
let base = ''
|
||||
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
|
||||
base = el.querySelector('.name').innerHTML + '/'
|
||||
}
|
||||
|
||||
if (base !== '') {
|
||||
api.fetch(this.$route.path + base)
|
||||
.then(req => {
|
||||
this.checkConflict(files, req.items, base)
|
||||
})
|
||||
.catch(error => { console.log(error) })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.checkConflict(files, this.req.items, base)
|
||||
},
|
||||
uploadInput: function (event) {
|
||||
this.handleFiles(event.currentTarget.files, '')
|
||||
checkConflict (files, items, base) {
|
||||
if (typeof items === 'undefined' || items === null) {
|
||||
items = []
|
||||
}
|
||||
|
||||
let conflict = false
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let res = items.findIndex(function hasConflict (element) {
|
||||
return (element.name === this)
|
||||
}, files[i].name)
|
||||
|
||||
if (res >= 0) {
|
||||
conflict = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!conflict) {
|
||||
this.handleFiles(files, base)
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.commit('showHover', {
|
||||
prompt: 'replace',
|
||||
confirm: (event) => {
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
this.handleFiles(files, base, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
resetOpacity: function () {
|
||||
uploadInput (event) {
|
||||
this.checkConflict(event.currentTarget.files, this.req.items, '')
|
||||
},
|
||||
resetOpacity () {
|
||||
let items = document.getElementsByClassName('item')
|
||||
|
||||
Array.from(items).forEach(file => {
|
||||
file.style.opacity = 1
|
||||
})
|
||||
},
|
||||
handleFiles: function (files, base) {
|
||||
this.resetOpacity()
|
||||
|
||||
handleFiles (files, base, overwrite = false) {
|
||||
buttons.loading('upload')
|
||||
let promises = []
|
||||
|
||||
for (let file of files) {
|
||||
promises.push(api.post(this.$route.path + base + file.name, file))
|
||||
promises.push(api.post(this.$route.path + base + file.name, file, overwrite))
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
buttons.done('upload')
|
||||
buttons.success('upload')
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -275,10 +342,14 @@ export default {
|
||||
if (this.nameIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
} else {
|
||||
} else if (sort === 'size') {
|
||||
if (this.sizeIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
} else if (sort === 'modified') {
|
||||
if (this.modifiedIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
}
|
||||
|
||||
let path = this.$store.state.baseURL
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
draggable="true"
|
||||
@dragstart="dragStart"
|
||||
@dragover="dragOver"
|
||||
@@ -7,6 +9,8 @@
|
||||
@click="click"
|
||||
@dblclick="open"
|
||||
@touchstart="touchstart"
|
||||
:data-dir="isDir"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected">
|
||||
<div>
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
@@ -29,7 +33,7 @@
|
||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'item',
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="previewer">
|
||||
<div class="bar">
|
||||
<button @click="back" class="action" aria-label="Close Preview" id="close">
|
||||
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
@@ -11,8 +11,12 @@
|
||||
<info-button></info-button>
|
||||
</div>
|
||||
|
||||
<button class="action" @click="prev" v-show="hasPrevious"><i class="material-icons">chevron_left</i></button>
|
||||
<button class="action" @click="next" v-show="hasNext"><i class="material-icons">chevron_right</i></button>
|
||||
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
||||
<i class="material-icons">chevron_left</i>
|
||||
</button>
|
||||
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</button>
|
||||
|
||||
<div class="preview">
|
||||
<img v-if="req.type == 'image'" :src="raw()">
|
||||
@@ -24,7 +28,7 @@
|
||||
</video>
|
||||
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object>
|
||||
<a v-else-if="req.type == 'blob'" :href="download()">
|
||||
<h2 class="message">Download <i class="material-icons">file_download</i></h2>
|
||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
<pre v-else >{{ req.content }}</pre>
|
||||
</div>
|
||||
@@ -34,11 +38,11 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
import InfoButton from './buttons/Info'
|
||||
import DeleteButton from './buttons/Delete'
|
||||
import RenameButton from './buttons/Rename'
|
||||
import DownloadButton from './buttons/Download'
|
||||
import * as api from '@/utils/api'
|
||||
import InfoButton from '@/components/buttons/Info'
|
||||
import DeleteButton from '@/components/buttons/Delete'
|
||||
import RenameButton from '@/components/buttons/Rename'
|
||||
import DownloadButton from '@/components/buttons/Download'
|
||||
|
||||
export default {
|
||||
name: 'preview',
|
||||
@@ -1,13 +1,19 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Copy</h3>
|
||||
<p>Choose the place to copy your files:</p>
|
||||
<h3>{{ $t('prompts.copy') }}</h3>
|
||||
<p>{{ $t('prompts.copyMessage') }}</p>
|
||||
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
|
||||
<div>
|
||||
<button class="ok" @click="copy">Copy</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
<button class="ok"
|
||||
@click="copy"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,7 +21,7 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import FileList from './FileList'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
@@ -45,7 +51,7 @@ export default {
|
||||
// Execute the promises.
|
||||
api.copy(items)
|
||||
.then(() => {
|
||||
buttons.done('copy')
|
||||
buttons.success('copy')
|
||||
this.$router.push({ path: this.dest })
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Delete files</h3>
|
||||
<p v-show="req.kind !== 'listing'">Are you sure you want to delete this file/folder?</p>
|
||||
<p v-show="req.kind === 'listing'">Are you sure you want to delete {{ selectedCount }} file(s)?</p>
|
||||
<h3>{{ $t('prompts.deleteTitle') }}</h3>
|
||||
<p v-show="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
||||
<p v-show="req.kind === 'listing'">{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
||||
<div>
|
||||
<button @click="submit" autofocus>Delete</button>
|
||||
<button @click="closeHovers" class="cancel">Cancel</button>
|
||||
<button @click="submit"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapMutations, mapState} from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
import { remove } from '@/utils/api'
|
||||
import url from '@/utils/url'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
@@ -31,9 +36,9 @@ export default {
|
||||
// If we are not on a listing, delete the current
|
||||
// opened file.
|
||||
if (this.req.kind !== 'listing') {
|
||||
api.delete(this.$route.path)
|
||||
remove(this.$route.path)
|
||||
.then(() => {
|
||||
buttons.done('delete')
|
||||
buttons.success('delete')
|
||||
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -54,12 +59,12 @@ export default {
|
||||
let promises = []
|
||||
|
||||
for (let index of this.selected) {
|
||||
promises.push(api.delete(this.req.items[index].url))
|
||||
promises.push(remove(this.req.items[index].url))
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
buttons.done('delete')
|
||||
buttons.success('delete')
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="prompt" id="download">
|
||||
<h3>Download files</h3>
|
||||
<p>Choose the format you want to download.</p>
|
||||
<h3>{{ $t('prompts.download') }}</h3>
|
||||
<p>{{ $t('prompts.downloadMessage') }}</p>
|
||||
|
||||
<button @click="download('zip')" autofocus>zip</button>
|
||||
<button @click="download('tar')" autofocus>tar</button>
|
||||
<button @click="download('targz')" autofocus>tar.gz</button>
|
||||
@@ -12,7 +13,7 @@
|
||||
|
||||
<script>
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'download',
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<template>
|
||||
<div class="prompt error">
|
||||
<i class="material-icons">error_outline</i>
|
||||
<h3>Something went wrong</h3>
|
||||
<h3>{{ $t('prompts.error') }}</h3>
|
||||
<pre>{{ $store.state.showMessage }}</pre>
|
||||
<div>
|
||||
<button @click="close" autofocus>Close</button>
|
||||
<button @click="reportIssue" class="cancel">Report Issue</button>
|
||||
<button @click="close"
|
||||
autofocus
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
<button @click="reportIssue"
|
||||
class="cancel"
|
||||
:aria-label="$t('buttons.reportIssue')"
|
||||
:title="$t('buttons.reportIssue')">{{ $t('buttons.reportIssue') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,19 +4,22 @@
|
||||
<li @click="select"
|
||||
@touchstart="touchstart"
|
||||
@dblclick="next"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="item.name"
|
||||
:aria-selected="selected == item.url"
|
||||
:key="item.name" v-for="item in items"
|
||||
:data-url="item.url">{{ item.name }}</li>
|
||||
</ul>
|
||||
|
||||
<p>Currently navigating on: <code>{{ nav }}</code>.</p>
|
||||
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'file-list',
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
<template>
|
||||
<div class="prompt help">
|
||||
<h3>Help</h3>
|
||||
<h3>{{ $t('help.help') }}</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>F1</strong> - this information</li>
|
||||
<li><strong>F2</strong> - rename file</li>
|
||||
<li><strong>DEL</strong> - delete selected items</li>
|
||||
<li><strong>ESC</strong> - clear selection and/or close the prompt</li>
|
||||
<li><strong>CTRL + S</strong> - save a file or download the directory where you are</li>
|
||||
<li><strong>CTRL + Click</strong> - select multiple files or directories</li>
|
||||
<li><strong>Double click</strong> - open a file or directory</li>
|
||||
<li><strong>Click</strong> - select file or directory</li>
|
||||
</ul>
|
||||
|
||||
<p>Not available yet</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Alt + Click</strong> - select a group of files</li>
|
||||
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
||||
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
||||
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
|
||||
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
|
||||
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
|
||||
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
|
||||
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
|
||||
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
||||
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="ok"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>File Information</h3>
|
||||
<h3>{{ $t('prompts.fileInfo') }}</h3>
|
||||
|
||||
<p v-show="selected.length > 1">{{ selected.length }} files selected.</p>
|
||||
<p v-show="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
||||
|
||||
<p v-show="selected.length < 2"><strong>Display Name:</strong> {{ name() }}</p>
|
||||
<p><strong>Size:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
||||
<p v-show="selected.length < 2"><strong>Last Modified:</strong> {{ humanTime() }}</p>
|
||||
<p v-show="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
|
||||
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
||||
<p v-show="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
|
||||
|
||||
<section v-show="dir() && selected.length === 0">
|
||||
<p><strong>Number of files:</strong> {{ req.numFiles }}</p>
|
||||
<p><strong>Number of directories:</strong> {{ req.numDirs }}</p>
|
||||
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
|
||||
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
|
||||
</section>
|
||||
|
||||
<section v-show="!dir()">
|
||||
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">show</a></code></p>
|
||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">show</a></code></p>
|
||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">show</a></code></p>
|
||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">show</a></code></p>
|
||||
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="ok"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -30,7 +34,7 @@
|
||||
import {mapState, mapGetters} from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'info',
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Move</h3>
|
||||
<p>Choose new house for your file(s)/folder(s):</p>
|
||||
<h3>{{ $t('prompts.move') }}</h3>
|
||||
<p>{{ $t('prompts.moveMessage') }}</p>
|
||||
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
|
||||
<div>
|
||||
<button class="ok" @click="move">Move</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
<button class="ok"
|
||||
@click="move"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,7 +21,7 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import FileList from './FileList'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
@@ -45,7 +51,7 @@ export default {
|
||||
// Execute the promises.
|
||||
api.move(items)
|
||||
.then(() => {
|
||||
buttons.done('move')
|
||||
buttons.success('move')
|
||||
this.$router.push({ path: this.dest })
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
68
assets/src/components/prompts/NewArchetype.vue
Normal file
68
assets/src/components/prompts/NewArchetype.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
||||
<p>{{ $t('prompts.newArchetype') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { removePrefix } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'new-archetype',
|
||||
data: function () {
|
||||
return {
|
||||
name: '',
|
||||
archetype: 'default'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
this.new('/' + this.name, this.archetype)
|
||||
.then((url) => {
|
||||
this.$router.push({ path: url })
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
},
|
||||
new (url, type) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
||||
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.getResponseHeader('Location'))
|
||||
} else {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>New directory</h3>
|
||||
<p>Write the name of the new directory.</p>
|
||||
<h3>{{ $t('prompts.newDir') }}</h3>
|
||||
<p>{{ $t('prompts.newDirMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok" @click="submit">Create</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
<button class="ok"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
@click="submit">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'new-dir',
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>New file</h3>
|
||||
<p>Write the name of the new file.</p>
|
||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
||||
<p>{{ $t('prompts.newFileMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok" @click="submit">Create</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'new-file',
|
||||
|
||||
@@ -11,27 +11,10 @@
|
||||
<copy v-else-if="showCopy"></copy>
|
||||
<error v-else-if="showError"></error>
|
||||
<success v-else-if="showSuccess"></success>
|
||||
|
||||
<template v-for="plugin in plugins">
|
||||
<form class="prompt"
|
||||
v-for="prompt in plugin.prompts"
|
||||
:key="prompt.name"
|
||||
v-if="show === prompt.name"
|
||||
@submit="prompt.submit($event, pluginData, $route)">
|
||||
<h3>{{ prompt.title }}</h3>
|
||||
<p>{{ prompt.description }}</p>
|
||||
<input v-for="input in prompt.inputs"
|
||||
:key="input.name"
|
||||
:type="input.type"
|
||||
:name="input.name"
|
||||
:placeholder="input.placeholder">
|
||||
<div>
|
||||
<input type="submit" class="ok" :value="prompt.ok">
|
||||
<button class="cancel" @click.prevent="$store.commit('closeHovers')">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<replace v-else-if="showReplace"></replace>
|
||||
<schedule v-else-if="show === 'schedule'"></schedule>
|
||||
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
|
||||
<share v-else-if="show === 'share'"></share>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -48,24 +31,32 @@ import Error from './Error'
|
||||
import Success from './Success'
|
||||
import NewFile from './NewFile'
|
||||
import NewDir from './NewDir'
|
||||
import NewArchetype from './NewArchetype'
|
||||
import Replace from './Replace'
|
||||
import Schedule from './Schedule'
|
||||
import Share from './Share'
|
||||
import { mapState } from 'vuex'
|
||||
import buttons from '@/utils/buttons'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'prompts',
|
||||
components: {
|
||||
Info,
|
||||
Delete,
|
||||
NewArchetype,
|
||||
Schedule,
|
||||
Rename,
|
||||
Error,
|
||||
Download,
|
||||
Success,
|
||||
Move,
|
||||
Copy,
|
||||
Share,
|
||||
NewFile,
|
||||
NewDir,
|
||||
Help
|
||||
Help,
|
||||
Replace
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
@@ -90,6 +81,7 @@ export default {
|
||||
showNewFile: function () { return this.show === 'newFile' },
|
||||
showNewDir: function () { return this.show === 'newDir' },
|
||||
showDownload: function () { return this.show === 'download' },
|
||||
showReplace: function () { return this.show === 'replace' },
|
||||
showOverlay: function () {
|
||||
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Rename</h3>
|
||||
<p>Insert a new name for <code>{{ oldName() }}</code>:</p>
|
||||
<h3>{{ $t('prompts.rename') }}</h3>
|
||||
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
||||
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button @click="submit" type="submit">Rename</button>
|
||||
<button @click="cancel" class="cancel">Cancel</button>
|
||||
<button @click="submit"
|
||||
type="submit"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,7 +20,7 @@
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'rename',
|
||||
|
||||
26
assets/src/components/prompts/Replace.vue
Normal file
26
assets/src/components/prompts/Replace.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.replace') }}</h3>
|
||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="showConfirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'replace',
|
||||
computed: mapState(['showConfirm'])
|
||||
}
|
||||
</script>
|
||||
41
assets/src/components/prompts/Schedule.vue
Normal file
41
assets/src/components/prompts/Schedule.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.schedule') }}</h3>
|
||||
<p>{{ $t('prompts.scheduleMessage') }}</p>
|
||||
<input autofocus type="datetime-local" v-model="date">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
|
||||
<button class="cancel"
|
||||
@click="close"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'schedule',
|
||||
data: function () {
|
||||
return {
|
||||
date: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
if (this.date === '') return
|
||||
this.close()
|
||||
this.$store.commit('setSchedule', this.date)
|
||||
document.getElementById('save-button').click()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
153
assets/src/components/prompts/Share.vue
Normal file
153
assets/src/components/prompts/Share.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="prompt" id="share">
|
||||
<h3>{{ $t('buttons.share') }}</h3>
|
||||
<p></p>
|
||||
<ul>
|
||||
<li v-if="!hasPermanent">
|
||||
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||
</li>
|
||||
|
||||
<li v-for="link in links" :key="link.hash">
|
||||
<a :href="buildLink(link.hash)" target="_blank">
|
||||
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</a>
|
||||
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
|
||||
<button class="action copy"
|
||||
:data-clipboard-text="buildLink(link.hash)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input autofocus
|
||||
type="number"
|
||||
max="2147483647"
|
||||
min="0"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="time">
|
||||
<select v-model="unit" :aria-label="$t('time.unit')">
|
||||
<option value="seconds">{{ $t('time.seconds') }}</option>
|
||||
<option value="minutes">{{ $t('time.minutes') }}</option>
|
||||
<option value="hours">{{ $t('time.hours') }}</option>
|
||||
<option value="days">{{ $t('time.days') }}</option>
|
||||
</select>
|
||||
<button class="action"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { getShare, deleteShare, share } from '@/utils/api'
|
||||
import moment from 'moment'
|
||||
import Clipboard from 'clipboard'
|
||||
|
||||
export default {
|
||||
name: 'share',
|
||||
data: function () {
|
||||
return {
|
||||
time: '',
|
||||
unit: 'hours',
|
||||
hasPermanent: false,
|
||||
links: [],
|
||||
clip: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'baseURL', 'req', 'selected', 'selectedCount' ]),
|
||||
url () {
|
||||
// Get the current name of the file we are editing.
|
||||
if (this.req.kind !== 'listing') {
|
||||
return this.$route.path
|
||||
}
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return
|
||||
}
|
||||
|
||||
return this.req.items[this.selected[0]].url
|
||||
}
|
||||
},
|
||||
beforeMount () {
|
||||
getShare(this.url)
|
||||
.then(links => {
|
||||
this.links = links
|
||||
this.sort()
|
||||
|
||||
for (let link of this.links) {
|
||||
if (!link.expires) {
|
||||
this.hasPermanent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error === 404) return
|
||||
this.showError(error)
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.clip = new Clipboard('.copy')
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showError' ]),
|
||||
submit: function (event) {
|
||||
if (!this.time) return
|
||||
|
||||
share(this.url, this.time, this.unit)
|
||||
.then(result => { this.links.push(result); this.sort() })
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
getPermalink (event) {
|
||||
share(this.url)
|
||||
.then(result => {
|
||||
this.links.push(result)
|
||||
this.sort()
|
||||
this.hasPermanent = true
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
deleteLink (event, link) {
|
||||
event.preventDefault()
|
||||
deleteShare(link.hash)
|
||||
.then(() => {
|
||||
if (!link.expires) this.hasPermanent = false
|
||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
humanTime (time) {
|
||||
return moment(time).fromNow()
|
||||
},
|
||||
buildLink (hash) {
|
||||
return `${window.location.origin}${this.baseURL}/share/${hash}`
|
||||
},
|
||||
sort () {
|
||||
this.links = this.links.sort((a, b) => {
|
||||
if (!a.expires) return -1
|
||||
if (!b.expires) return 1
|
||||
return new Date(a.expireDate) - new Date(b.expireDate)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
<i class="material-icons">done</i>
|
||||
<h3>{{ $store.state.showMessage }}</h3>
|
||||
<div>
|
||||
<button @click="close" autofocus>OK</button>
|
||||
<button @click="close"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')"
|
||||
autofocus>{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -133,5 +133,15 @@ main {
|
||||
}
|
||||
|
||||
#breadcrumbs a {
|
||||
color: inherit
|
||||
color: inherit;
|
||||
transition: .1s ease-in;
|
||||
border-radius: .125em;
|
||||
}
|
||||
|
||||
#breadcrumbs a:hover {
|
||||
background-color: rgba(0,0,0, 0.05);
|
||||
}
|
||||
|
||||
#breadcrumbs span a {
|
||||
padding: .2em;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
width: 1em
|
||||
}
|
||||
|
||||
.dashboard > *:first-child {
|
||||
.dashboard > h1:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ form.dashboard > p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dashboard select,
|
||||
.dashboard textarea,
|
||||
.dashboard input[type="text"],
|
||||
.dashboard input[type="password"] {
|
||||
@@ -60,12 +61,18 @@ form.dashboard > p:last-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard #locale,
|
||||
.dashboard #username,
|
||||
.dashboard #password,
|
||||
.dashboard #scope {
|
||||
max-width: 18em;
|
||||
}
|
||||
|
||||
.dashboard #locale {
|
||||
border: 1px solid #dddddd;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
.dashboard textarea:focus,
|
||||
.dashboard textarea:hover,
|
||||
.dashboard input[type="text"]:focus,
|
||||
@@ -118,3 +125,27 @@ p code {
|
||||
font-size: .8em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dashboard #nav {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
color: rgb(84, 110, 122);
|
||||
font-weight: 500;
|
||||
padding: 0 0 1em;
|
||||
margin: 0 0 1em;
|
||||
font-size: .8em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dashboard #nav li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard #nav li:last-child {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.dashboard #nav i {
|
||||
font-size: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ header>div div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header > div:last-child div {
|
||||
header>div:last-child div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -129,10 +129,10 @@ header .search-button {
|
||||
#search #result {
|
||||
visibility: visible;
|
||||
max-height: none;
|
||||
background-color: #fff;
|
||||
background-color: #f8f8f8;
|
||||
text-align: left;
|
||||
color: #ccc;
|
||||
padding: 0;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
height: 0;
|
||||
transition: .1s ease height, .1s ease padding;
|
||||
overflow-x: hidden;
|
||||
@@ -140,6 +140,10 @@ header .search-button {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#search #result>div>*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#search.active #result {
|
||||
padding: .5em;
|
||||
height: calc(100% - 4em);
|
||||
@@ -155,15 +159,12 @@ header .search-button {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
#search #result div {
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
word-wrap: break-word;
|
||||
#search #result>div {
|
||||
max-width: 45em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#search #result p {
|
||||
#search #result #renew {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: none;
|
||||
@@ -171,17 +172,30 @@ header .search-button {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#search.ongoing #result p {
|
||||
#search.ongoing #result #renew {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#search.active #result i {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#search.active #result>p>i {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
display: table;
|
||||
}
|
||||
|
||||
#search.active #result ul li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .3em 0;
|
||||
}
|
||||
|
||||
#search.active #result ul li a i {
|
||||
margin-right: .3em;
|
||||
}
|
||||
|
||||
#search::-webkit-input-placeholder {
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
@@ -199,3 +213,47 @@ header .search-button {
|
||||
#search:-ms-input-placeholder {
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
|
||||
#search .boxes {
|
||||
border: 1px solid rgba(0, 0, 0, 0.075);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
background: #fff;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#search .boxes h3 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
font-size: 1em;
|
||||
color: #212121;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
#search .boxes>div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-right: -1em;
|
||||
margin-bottom: -1em;
|
||||
}
|
||||
|
||||
#search .boxes>div>div {
|
||||
background: #2196F3;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
width: 10em;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
margin-bottom: 1em;
|
||||
margin-right: 1em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#search .boxes p {
|
||||
margin: 1em 0 0;
|
||||
}
|
||||
|
||||
#search .boxes i {
|
||||
color: #fff !important;
|
||||
font-size: 3.5em;
|
||||
}
|
||||
|
||||
68
assets/src/css/login.css
Normal file
68
assets/src/css/login.css
Normal file
@@ -0,0 +1,68 @@
|
||||
#login {
|
||||
background: #fff;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#login img {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#login h1 {
|
||||
text-align: center;
|
||||
font-size: 2.5em;
|
||||
margin: .4em 0 .67em;
|
||||
}
|
||||
|
||||
#login form {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 16em;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#login input {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
#login .wrong {
|
||||
background: #F44336;
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
}
|
||||
|
||||
@keyframes opac {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#login input[type="text"],
|
||||
#login input[type="password"] {
|
||||
padding: .5em 1em;
|
||||
border: 1px solid #e9e9e9;
|
||||
transition: .2s ease border;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#login input[type="text"]:focus,
|
||||
#login input[type="password"]:focus,
|
||||
#login input[type="text"]:hover,
|
||||
#login input[type="password"]:hover {
|
||||
border-color: #9f9f9f;
|
||||
}
|
||||
@@ -61,7 +61,7 @@
|
||||
background: #fff;
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
width: 95%;
|
||||
max-width: 18em;
|
||||
max-width: 20em;
|
||||
}
|
||||
#file-selection .action {
|
||||
border-radius: 50%;
|
||||
|
||||
@@ -177,3 +177,32 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt#share ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.prompt#share ul li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.prompt#share ul li a {
|
||||
color: #2196F3;
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.prompt#share ul li .action i {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.prompt#share ul li input,
|
||||
.prompt#share ul li select {
|
||||
padding: .2em;
|
||||
margin-right: .5em;
|
||||
border: 1px solid #dadada;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@import "./listing.css";
|
||||
@import "./editor.css";
|
||||
@import "./dashboard.css";
|
||||
@import "./login.css";
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* ACTION *
|
||||
|
||||
196
assets/src/i18n/en.yaml
Normal file
196
assets/src/i18n/en.yaml
Normal file
@@ -0,0 +1,196 @@
|
||||
permanent: Permanent
|
||||
buttons:
|
||||
cancel: Cancel
|
||||
close: Close
|
||||
copy: Copy
|
||||
copyFile: Copy file
|
||||
copyToClipboard: Copy to clipboard
|
||||
create: Create
|
||||
delete: Delete
|
||||
download: Download
|
||||
info: Info
|
||||
more: More
|
||||
move: Move
|
||||
moveFile: Move file
|
||||
new: New
|
||||
next: Next
|
||||
ok: OK
|
||||
replace: Replace
|
||||
previous: Previous
|
||||
rename: Rename
|
||||
reportIssue: Report Issue
|
||||
save: Save
|
||||
search: Search
|
||||
select: Select
|
||||
share: Share
|
||||
publish: Publish
|
||||
selectMultiple: Select multiple
|
||||
schedule: Schedule
|
||||
switchView: Swicth view
|
||||
toggleSidebar: Toggle sidebar
|
||||
update: Update
|
||||
upload: Upload
|
||||
permalink: Get Permanent Link
|
||||
errors:
|
||||
forbidden: You're not welcome here.
|
||||
internal: Something really went wrong.
|
||||
notFound: This location can't be reached.
|
||||
files:
|
||||
folders: Folders
|
||||
files: Files
|
||||
body: Body
|
||||
clear: Clear
|
||||
closePreview: Close preview
|
||||
home: Home
|
||||
lastModified: Last modified
|
||||
loading: Loading...
|
||||
lonely: It feels lonely here...
|
||||
metadata: Metadata
|
||||
multipleSelectionEnabled: Multiple selection enabled
|
||||
name: Name
|
||||
size: Size
|
||||
sortByName: Sort by name
|
||||
sortBySize: Sort by size
|
||||
sortByLastModified: Sort by last modified
|
||||
help:
|
||||
click: select file or directory
|
||||
ctrl:
|
||||
click: select multiple files or directories
|
||||
f: opens search
|
||||
s: save a file or download the directory where you are
|
||||
del: delete selected items
|
||||
doubleClick: open a file or directory
|
||||
esc: clear selection and/or close the prompt
|
||||
f1: this information
|
||||
f2: rename file
|
||||
help: Help
|
||||
login:
|
||||
password: Password
|
||||
submit: Login
|
||||
username: Username
|
||||
wrongCredentials: Wrong credentials
|
||||
prompts:
|
||||
copy: Copy
|
||||
copyMessage: 'Choose the place to copy your files:'
|
||||
currentlyNavigating: 'Currently navigating on:'
|
||||
deleteMessageMultiple: Are you sure you want to delete {count} file(s)?
|
||||
deleteMessageSingle: Are you sure you want to delete this file/folder?
|
||||
deleteTitle: Delete files
|
||||
displayName: 'Display Name:'
|
||||
download: Download files
|
||||
downloadMessage: Choose the format you want to download.
|
||||
error: Something went wrong
|
||||
fileInfo: File information
|
||||
filesSelected: "{count} files selected."
|
||||
lastModified: Last Modified
|
||||
move: Move
|
||||
moveMessage: 'Choose new house for your file(s)/folder(s):'
|
||||
newDir: New directory
|
||||
newDirMessage: Write the name of the new directory.
|
||||
newFile: New file
|
||||
newFileMessage: Write the name of the new file.
|
||||
numberDirs: Number of directories
|
||||
numberFiles: Number of files
|
||||
replace: Replace
|
||||
replaceMessage: >
|
||||
One of the files you're trying to upload is conflicting because of its name.
|
||||
Do you wish to replace the existing one?
|
||||
rename: Rename
|
||||
renameMessage: Insert a new name for
|
||||
show: Show
|
||||
size: Size
|
||||
schedule: Schedule
|
||||
scheduleMessage: Pick a date and time to schedule the publication of this post.
|
||||
newArchetype: Create a new post based on an archetype. Your file will be created on content folder.
|
||||
settings:
|
||||
admin: Admin
|
||||
administrator: Administrator
|
||||
allowCommands: Execute commands
|
||||
allowEdit: Edit, rename and delete files or directories
|
||||
allowNew: Create new files and directories
|
||||
allowPublish: Publish new posts and pages
|
||||
avoidChanges: "(leave blank to avoid changes)"
|
||||
changePassword: Change Password
|
||||
commands: Commands
|
||||
commandsHelp: >
|
||||
Here you can set commands that are executed in the named events. You
|
||||
write one command per line. If the event is related to files, such as before and
|
||||
after saving, the environment variable "file" will be available with the path
|
||||
of the file.
|
||||
commandsUpdated: Commands updated!
|
||||
customStylesheet: Custom Stylesheet
|
||||
examples: Examples
|
||||
globalSettings: Global Settings
|
||||
language: Language
|
||||
newPassword: Your new password
|
||||
newPasswordConfirm: Confirm your new password
|
||||
newUser: New User
|
||||
password: Password
|
||||
passwordUpdated: Password updated!
|
||||
permissions: Permissions
|
||||
permissionsHelp: >
|
||||
You can set the user to be an administrator or choose the permissions
|
||||
individually. If you select "Administrator", all of the other options will be
|
||||
automatically checked. The management of users remains a privilege of an administrator.
|
||||
profileSettings: Profile Settings
|
||||
ruleExample1: >
|
||||
'prevents the access to any dot file (such as .git, .gitignore) in
|
||||
every folder.'
|
||||
ruleExample2: blocks the access to the file named Caddyfile on the root of the scope.
|
||||
rules: Rules
|
||||
rulesHelp1: >
|
||||
'Here you can define a set of allow and disallow rules for this specific
|
||||
user. The blocked files won''t show up in the listings and they won''t be accessible
|
||||
to the user. We support regex and paths relative to the user''s scope.'
|
||||
rulesHelp2: >
|
||||
Each rule goes in one different line and must start with the keyword
|
||||
{0} or {1}. Then you should write {2} if you are using a regular expression and
|
||||
then the expression or the path.
|
||||
scope: Scope
|
||||
settingsUpdated: Settings updated!
|
||||
user: User
|
||||
userCommands: Commands
|
||||
userCommandsHelp:
|
||||
'A space separated list with the available commands for this user.
|
||||
Example:'
|
||||
userCreated: User created!
|
||||
userDeleted: User deleted!
|
||||
userManagement: User Management
|
||||
username: Username
|
||||
users: Users
|
||||
userUpdated: User updated!
|
||||
sidebar:
|
||||
help: Help
|
||||
logout: Logout
|
||||
myFiles: My files
|
||||
newFile: New file
|
||||
newFolder: New folder
|
||||
servedWith: Served with
|
||||
settings: Settings
|
||||
siteSettings: Site Settings
|
||||
hugoNew: Hugo New
|
||||
preview: Preview
|
||||
search:
|
||||
images: Images
|
||||
music: Music
|
||||
pdf: PDF
|
||||
pressToExecute: Press enter to execute.
|
||||
pressToSearch: Press enter to search.
|
||||
search: Search...
|
||||
searchOrCommand: Search or execute a command...
|
||||
searchOrSupportedCommand: 'Search or use one of your supported commands:'
|
||||
type: Type and press enter to search.
|
||||
types: Types
|
||||
video: Video
|
||||
writeToSearch: Write here to search
|
||||
languages:
|
||||
en: English
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
time:
|
||||
unit: Time Unit
|
||||
seconds: Seconds
|
||||
minutes: Minutes
|
||||
hours: Hours
|
||||
days: Days
|
||||
21
assets/src/i18n/index.js
Normal file
21
assets/src/i18n/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import en from './en.yaml'
|
||||
import pt from './pt.yaml'
|
||||
import ja from './ja.yaml'
|
||||
import zhCN from './zh-cn.yaml'
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
'en': en,
|
||||
'pt': pt,
|
||||
'ja': ja,
|
||||
'zh-cn': zhCN
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
193
assets/src/i18n/ja.yaml
Normal file
193
assets/src/i18n/ja.yaml
Normal file
@@ -0,0 +1,193 @@
|
||||
permanent: 永久
|
||||
buttons:
|
||||
cancel: キャンセル
|
||||
close: 閉じる
|
||||
copy: コピー
|
||||
copyFile: ファイルをコピー
|
||||
copyToClipboard: クリップボードにコピー
|
||||
create: 作成
|
||||
delete: 削除
|
||||
download: ダウンロード
|
||||
info: 情報
|
||||
more: More
|
||||
move: 移動
|
||||
moveFile: ファイルを移動
|
||||
new: 新規
|
||||
next: 次
|
||||
ok: OK
|
||||
replace: 置き換える
|
||||
previous: 前
|
||||
rename: 名前を変更
|
||||
reportIssue: 問題を報告
|
||||
save: 保存
|
||||
search: 検索
|
||||
select: 選択
|
||||
share: シェア
|
||||
publish: 発表
|
||||
selectMultiple: 複数選択
|
||||
schedule: スケジュール
|
||||
switchView: 表示を切り替わる
|
||||
toggleSidebar: サイドバーを表示する
|
||||
update: 更新
|
||||
upload: アップロード
|
||||
permalink: 固定リンク
|
||||
errors:
|
||||
forbidden: アクセスが拒否されました。
|
||||
internal: 内部エラーが発生しました。
|
||||
notFound: リソースが見つからなりませんでした。
|
||||
files:
|
||||
folders: フォルダ
|
||||
files: ファイル
|
||||
body: 本文
|
||||
clear: クリアー
|
||||
closePreview: プレビューを閉じる
|
||||
home: ホーム
|
||||
lastModified: 最終変更
|
||||
loading: ローディング...
|
||||
lonely: ここには何もない...
|
||||
metadata: メタデータ
|
||||
multipleSelectionEnabled: 複数選択有効
|
||||
name: 名前
|
||||
size: サイズ
|
||||
sortByName: 名前によるソート
|
||||
sortBySize: サイズによるソート
|
||||
sortByLastModified: 最終変更日付によるソート
|
||||
help:
|
||||
click: ファイルやディレクトリを選択
|
||||
ctrl:
|
||||
click: 複数のファイルやディレクトリを選択
|
||||
f: 検索を有効にする
|
||||
s: ファイルを保存またはカレントディレクトリをダウンロード
|
||||
del: 選択した項目を削除
|
||||
doubleClick: ファイルやディレクトリをオープン
|
||||
esc: 選択をクリアーまたはプロンプトを閉じる
|
||||
f1: このヘルプを表示
|
||||
f2: ファイルの名前を変更
|
||||
help: ヘルプ
|
||||
login:
|
||||
password: パスワード
|
||||
submit: ログイン
|
||||
username: ユーザ名
|
||||
wrongCredentials: ユーザ名またはパスワードが間違っています。
|
||||
prompts:
|
||||
copy: コピー
|
||||
copyMessage: コピーの目標ディレクトリを選択してください:
|
||||
currentlyNavigating: 現在閲覧しているディレクトリ:
|
||||
deleteMessageMultiple: '{count} つのファイルを本当に削除してよろしいですか。'
|
||||
deleteMessageSingle: このファイル/フォルダを本当に削除してよろしいですか。
|
||||
deleteTitle: ファイルを削除
|
||||
displayName: 名前:
|
||||
download: ファイルをダウンロード
|
||||
downloadMessage: 圧縮形式を選択してください。
|
||||
error: あるエラーが発生しました。
|
||||
fileInfo: ファイル情報
|
||||
filesSelected: '{count} つのファイルは選択されました。'
|
||||
lastModified: 最終変更
|
||||
move: 移動
|
||||
moveMessage: 移動の目標ディレクトリを選択してください:
|
||||
newDir: 新しいディレクトリを作成
|
||||
newDirMessage: 新しいディレクトリの名前を入力してください。
|
||||
newFile: 新しいファイルを作成
|
||||
newFileMessage: 新しいファイルの名前を入力してください。
|
||||
numberDirs: ディレクトリ個数
|
||||
numberFiles: ファイル個数
|
||||
replace: 置き換える
|
||||
replaceMessage: >
|
||||
アップロードするファイルの中でかち合う名前が一つあります。
|
||||
既存のファイルを置き換えりませんか。
|
||||
rename: 名前を変更
|
||||
renameMessage: 名前を変更しようファイルは:
|
||||
show: 表示
|
||||
size: サイズ
|
||||
schedule: スケジュール
|
||||
scheduleMessage: このポストの発表日付をスケジュールしてください。
|
||||
newArchetype: ある元型に基づいて新しいポストを作成します。ファイルは コンテンツフォルダに作成されます。
|
||||
settings:
|
||||
admin: 管理者
|
||||
administrator: 管理者
|
||||
allowCommands: コマンドの実行
|
||||
allowEdit: ファイルやディレクトリの編集、名前変更と削除
|
||||
allowNew: ファイルとディレクトリの作成
|
||||
allowPublish: ポストとぺーじの発表
|
||||
avoidChanges: "(変更を避けるために空白にしてください)"
|
||||
changePassword: パスワードを変更
|
||||
commands: コマンド
|
||||
commandsHelp: >
|
||||
ここで、名前付きイベントに実行するコマンドを設定することができます。
|
||||
一行にコマンド一つを入力してください。
|
||||
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、環境変数 file はファイルのパスに割り当てられます。
|
||||
commandsUpdated: コマンドは更新されました!
|
||||
customStylesheet: カスタムスタイルシ ート
|
||||
examples: 例
|
||||
globalSettings: グローバル設定
|
||||
language: 言語
|
||||
newPassword: 新しいパスワード
|
||||
newPasswordConfirm: 新しいパスワードを確認します
|
||||
newUser: 新しいユーザー
|
||||
password: パスワード
|
||||
passwordUpdated: パスワードは更新されました!
|
||||
permissions: 権限
|
||||
permissionsHelp: >
|
||||
あなたはユーザーを管理者に設定し、または権限を個々に設定しできます。
|
||||
"管理者"を選択する場合、その他のすべての選択肢は自動的に設定されます。
|
||||
ユーザーの管理は管理者の権限として保留されました。
|
||||
profileSettings: プロファイル設定
|
||||
ruleExample1: >
|
||||
'各フォルダに名前はドットで始まるファイル(例えば、.git、.gitignore)へのアクセスを制限します。'
|
||||
ruleExample2: 範囲のルートパスに名前は Caddyfile のファイルへのアクセスを制限します。
|
||||
rules: 規則
|
||||
rulesHelp1: >
|
||||
'ここに、あなたはこのユーザーの許可または拒否規則を設定できます。
|
||||
ブロックされたファイルはリストに表示されません、それではアクセスも制限されます。
|
||||
正規表現(regex)のサポートと範囲に相対のパスが提供されています。'
|
||||
rulesHelp2: >
|
||||
一行に規則一つを入力してください、その間に規則はキーワード {0} や {1} で始める必要があります。
|
||||
そして正規表現を使う場合、{2} と入力し、表現やパスを入力してください。
|
||||
scope: 範囲
|
||||
settingsUpdated: 設定は更新されました!
|
||||
user: ユーザー
|
||||
userCommands: ユーザーのコマンド
|
||||
userCommandsHelp: >
|
||||
空白区切りの有効のコマンドのリストを指定してください。
|
||||
例:
|
||||
userCreated: ユーザーは作成されました!
|
||||
userDeleted: ユーザーは削除されました!
|
||||
userManagement: ユーザー管理
|
||||
username: ユーザー名
|
||||
users: ユーザー
|
||||
userUpdated: ユーザーは更新されました!
|
||||
sidebar:
|
||||
help: ヘルプ
|
||||
logout: ログアウト
|
||||
myFiles: 私のファイル
|
||||
newFile: 新しいファイルを作成
|
||||
newFolder: 新しいフォルダを作成
|
||||
servedWith: サービス提供者
|
||||
settings: 設定
|
||||
siteSettings: サイト設定
|
||||
hugoNew: Hugo New
|
||||
preview: プレビュー
|
||||
search:
|
||||
images: 画像
|
||||
music: 音楽
|
||||
pdf: PDF
|
||||
pressToExecute: Enter を押して実行します。
|
||||
pressToSearch: Enter を押して検索します。
|
||||
search: 検索...
|
||||
searchOrCommand: コマンドを検索または実行します。
|
||||
searchOrSupportedCommand: サポートしているコマンドを検索または実行します:
|
||||
type: キーワードを入力し、Enter を押して検索します。
|
||||
types: 種類
|
||||
video: ビデオ
|
||||
writeToSearch: ここにキーワードを入力してください
|
||||
languages:
|
||||
en: English
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
time:
|
||||
unit: 時間単位
|
||||
seconds: 秒
|
||||
minutes: 分
|
||||
hours: 時間
|
||||
days: 日
|
||||
199
assets/src/i18n/pt.yaml
Normal file
199
assets/src/i18n/pt.yaml
Normal file
@@ -0,0 +1,199 @@
|
||||
permanent: Permanente
|
||||
buttons:
|
||||
cancel: Cancelar
|
||||
close: Fechar
|
||||
copy: Copiar
|
||||
copyFile: Copiar ficheiro
|
||||
copyToClipboard: Copiar
|
||||
create: Criar
|
||||
delete: Eliminar
|
||||
download: Descarregar
|
||||
info: Info
|
||||
more: Mais
|
||||
move: Mover
|
||||
moveFile: Mover ficheiro
|
||||
new: Novo
|
||||
next: Próximo
|
||||
ok: OK
|
||||
previous: Anterior
|
||||
publish: Publicar
|
||||
rename: Renomear
|
||||
replace: Substituir
|
||||
reportIssue: Reportar Erro
|
||||
save: Guardar
|
||||
share: Partilhar
|
||||
schedule: Agendar
|
||||
search: Pesquisar
|
||||
select: Selecionar
|
||||
selectMultiple: Selecionar múltiplos
|
||||
switchView: Alterar modo de visão
|
||||
toggleSidebar: Alternar barra lateral
|
||||
update: Atualizar
|
||||
upload: Enviar
|
||||
permalink: Obter link permanente
|
||||
errors:
|
||||
forbidden: Tu não és bem-vindo aqui.
|
||||
internal: Algo correu bastante mal.
|
||||
notFound: Não conseguimos chegar a esta localização.
|
||||
files:
|
||||
body: Corpo
|
||||
clear: Limpar
|
||||
closePreview: Fechar pré-visualização
|
||||
files: Ficheiros
|
||||
folders: Pastas
|
||||
home: Início
|
||||
lastModified: Última modificação
|
||||
loading: A carregar...
|
||||
lonely: Sinto-me sozinho...
|
||||
metadata: Metadados
|
||||
multipleSelectionEnabled: Seleção múltipla ativada
|
||||
name: Nome
|
||||
size: Tamanho
|
||||
sortByLastModified: Ordenar pela última modificação
|
||||
sortByName: Ordenar pelo nome
|
||||
sortBySize: Ordenar pelo tamanho
|
||||
help:
|
||||
click: selecionar pasta ou ficheiro
|
||||
ctrl:
|
||||
click: selecionar várias pastas e ficheiros
|
||||
f: pesquisar
|
||||
s: guardar um ficheiro ou descarregar a pasta em que estás a navegar
|
||||
del: eliminar os ficheiros selecionados
|
||||
doubleClick: abrir pasta ou ficheiro
|
||||
esc: limpar seleção e/ou fechar menu
|
||||
f1: esta informação
|
||||
f2: renomear ficheiro
|
||||
help: Ajuda
|
||||
languages:
|
||||
en: English
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
login:
|
||||
password: Palavra-passe
|
||||
submit: Login
|
||||
username: Nome de utilizador
|
||||
wrongCredentials: Dados errados
|
||||
prompts:
|
||||
copy: Copiar
|
||||
copyMessage: 'Escolhe um lugar para copiar os ficheiros:'
|
||||
currentlyNavigating: 'A navegar em:'
|
||||
deleteMessageMultiple: Deseja eliminar {count} ficheiro(s)?
|
||||
deleteMessageSingle: Deseja eliminar esta pasta/ficheiro?
|
||||
deleteTitle: Eliminar ficheiros
|
||||
displayName: 'Nome:'
|
||||
download: Descarregar ficheiros
|
||||
downloadMessage: Escolha o formato do ficheiro.
|
||||
error: Algo correu mal
|
||||
fileInfo: Informação do ficheiro
|
||||
filesSelected: "{count} ficheiros selecionados."
|
||||
lastModified: Última Modificação
|
||||
move: Mover
|
||||
moveMessage: 'Escolha uma nova casa para os seus ficheiros:'
|
||||
newArchetype: Criar um novo post baseado num "archetype". O seu ficheiro será criado
|
||||
na pasta "content".
|
||||
newDir: Nova pasta
|
||||
newDirMessage: Escreva o nome da nova pasta.
|
||||
newFile: Novo ficheiro
|
||||
newFileMessage: Escreva o nome do novo ficheiro.
|
||||
numberDirs: Número de pastas
|
||||
numberFiles: Número de ficheiros
|
||||
rename: Renomear
|
||||
renameMessage: Insira um novo nome para
|
||||
replace: Substituir
|
||||
replaceMessage: >
|
||||
Já existe um ficheiro com nome igual a um dos que está a tentar
|
||||
enviar. Deseja substituir?
|
||||
schedule: Agendar
|
||||
scheduleMessage: Escolha uma data para publicar este post.
|
||||
show: Mostrar
|
||||
size: Tamanho
|
||||
search:
|
||||
images: Imagens
|
||||
music: Música
|
||||
pdf: PDF
|
||||
pressToExecute: Prima enter para executar.
|
||||
pressToSearch: Prima enter para pesquisar.
|
||||
search: Pesquise...
|
||||
searchOrCommand: Pesquise ou execute um comando...
|
||||
searchOrSupportedCommand: 'Pesquise ou utilize um dos seus comandos:'
|
||||
type: Escreva e prima enter para pesquisar.
|
||||
types: Tipos
|
||||
video: Vídeos
|
||||
writeToSearch: Escreva aqui para pesquisar
|
||||
settings:
|
||||
admin: Admin
|
||||
administrator: Administrador
|
||||
allowCommands: Executar comandos
|
||||
allowEdit: Editar, renomear e eliminar ficheiros ou pastas
|
||||
allowNew: Criar novos ficheiros e pastas
|
||||
allowPublish: Publicar novas páginas e conteúdos
|
||||
avoidChanges: "(deixe em branco para manter)"
|
||||
changePassword: Alterar Password
|
||||
commands: Comandos
|
||||
commandsHelp: >
|
||||
Pode definir um conjunto de comandos a executar em determiandos eventos.
|
||||
Deve escrever um comando por linha. Se o evento estiver relacionado com ficheiros,
|
||||
como antes e depois de guardar, irá existir uma variável de ambiente denominada
|
||||
"file" com o caminho do ficheiro.
|
||||
commandsUpdated: Comandos atualizados!
|
||||
customStylesheet: Estilos Personalizados
|
||||
examples: Exemplos
|
||||
globalSettings: Configurações Globais
|
||||
language: Linguagem
|
||||
newPassword: Nova palavra-passe
|
||||
newPasswordConfirm: Confirme a nova palavra-passe
|
||||
newUser: Novo Utilizador
|
||||
password: Palavra-passe
|
||||
passwordUpdated: Palavra-passe atualizada!
|
||||
permissions: Permissões
|
||||
permissionsHelp: >
|
||||
Pode definir o utilizador como administrador ou escolher as permissões
|
||||
manualmente. Se selecionar a opção "Administrador", todas as outras opções serão
|
||||
automaticamente selecionadas. A gestão dos utilizadores é um privilégio restringido
|
||||
aos administradores.
|
||||
profileSettings: Configurações do Utilizador
|
||||
ruleExample1: >
|
||||
previne o acesso a qualquer "dotfile" (como .git, .gitignore) em
|
||||
qualquer pasta
|
||||
ruleExample2: bloqueia o acesso ao ficheiro chamado Caddyfile.
|
||||
rules: Regras
|
||||
rulesHelp1: >
|
||||
Aqui pode definir um conjunto de regras para permitir ou bloquear o
|
||||
acesso do utilizador a determinados ficheiros ou pastas. Os ficheiros bloqueados
|
||||
não irão aparecer durante a navegação. Suportamos expressões regulares e os caminhos
|
||||
dos ficheiros devem ser relativos à base do utilizador.
|
||||
rulesHelp2: >
|
||||
Cada regra deve ser colocada numa linha diferente e deve começar com
|
||||
as palavras {0} (permite) ou {1} (bloqueia). Deve escrever, logo de seguida, {2},
|
||||
caso queira utilizar uma expressão regular. Depois, escreva o caminho do ficheiro/pasta
|
||||
ou a expressão regular.
|
||||
scope: Base
|
||||
settingsUpdated: Configurações atualizadas!
|
||||
user: Utilizador
|
||||
userCommands: Comandos
|
||||
userCommandsHelp: 'Uma lista, separada com espaços, de comandos disponíveis para
|
||||
este utilizados. Exemplo:'
|
||||
userCreated: Utilizador criado!
|
||||
userDeleted: Utilizador eliminado!
|
||||
userManagement: Gestão de Utilizadores
|
||||
username: Nome de utilizador
|
||||
users: Utilizadores
|
||||
userUpdated: Utilizador atualizado!
|
||||
sidebar:
|
||||
help: Ajuda
|
||||
hugoNew: Hugo New
|
||||
logout: Sair
|
||||
myFiles: Ficheiros
|
||||
newFile: Novo ficheiro
|
||||
newFolder: Nova pasta
|
||||
preview: Pré-visualizar
|
||||
servedWith: Servido com
|
||||
settings: Configurações
|
||||
siteSettings: Configurações do Site
|
||||
time:
|
||||
unit: Unidades de Tempo
|
||||
seconds: Segundos
|
||||
minutes: Minutos
|
||||
hours: Horas
|
||||
days: Dias
|
||||
193
assets/src/i18n/zh-cn.yaml
Normal file
193
assets/src/i18n/zh-cn.yaml
Normal file
@@ -0,0 +1,193 @@
|
||||
permanent: 永久
|
||||
buttons:
|
||||
cancel: 取消
|
||||
close: 关闭
|
||||
copy: 复制
|
||||
copyFile: 复制文件
|
||||
copyToClipboard: 复制到剪贴板
|
||||
create: 创建
|
||||
delete: 删除
|
||||
download: 下载
|
||||
info: 信息
|
||||
more: 更多
|
||||
move: 移动
|
||||
moveFile: 移动文件
|
||||
new: 新
|
||||
next: 下一页
|
||||
ok: 确定
|
||||
replace: 替换
|
||||
previous: 上一页
|
||||
rename: 重命名
|
||||
reportIssue: 报告问题
|
||||
save: 保存
|
||||
search: 搜索
|
||||
select: 选择
|
||||
share: 分享
|
||||
publish: 发布
|
||||
selectMultiple: 选择多个
|
||||
schedule: 计划
|
||||
switchView: 切换显示方式
|
||||
toggleSidebar: 切换侧边栏
|
||||
update: 更新
|
||||
upload: 上传
|
||||
permalink: 获取永久链接
|
||||
errors:
|
||||
forbidden: 你被禁止访问。
|
||||
internal: 内部出现麻烦了。
|
||||
notFound: 找不到文件。
|
||||
files:
|
||||
folders: 文件夹
|
||||
files: 文件
|
||||
body: Body
|
||||
clear: 清空
|
||||
closePreview: 关闭预览
|
||||
home: 主页
|
||||
lastModified: 最后修改
|
||||
loading: 加载中...
|
||||
lonely: 这里没有任何文件...
|
||||
metadata: 元数据
|
||||
multipleSelectionEnabled: 多选模式已开启
|
||||
name: 名称
|
||||
size: 大小
|
||||
sortByName: 按名称排序
|
||||
sortBySize: 按大小排序
|
||||
sortByLastModified: 按最后修改时间排序
|
||||
help:
|
||||
click: 选择文件或目录
|
||||
ctrl:
|
||||
click: 选择多个文件或目录
|
||||
f: 打开搜索框
|
||||
s: 保存文件或下载当前文件夹
|
||||
del: 删除所选的文件/文件夹
|
||||
doubleClick: 打开文件/文件夹
|
||||
esc: 清除已选项或关闭提示信息
|
||||
f1: 显示该帮助信息
|
||||
f2: 重命名文件/文件夹
|
||||
help: 帮助
|
||||
login:
|
||||
password: 密码
|
||||
submit: 登录
|
||||
username: 用户名
|
||||
wrongCredentials: 用户名或密码错误
|
||||
prompts:
|
||||
copy: 复制
|
||||
copyMessage: 请选择欲复制至的目录:
|
||||
currentlyNavigating: 当前目录:
|
||||
deleteMessageMultiple: 你确定要删除这 {count} 个文件吗?
|
||||
deleteMessageSingle: 你确定要删除这个文件/文件夹吗?
|
||||
deleteTitle: 删除文件
|
||||
displayName: 名称:
|
||||
download: 下载文件
|
||||
downloadMessage: 请选择要下载的压缩格式。
|
||||
error: 出了一点问题...
|
||||
fileInfo: 文件信息
|
||||
filesSelected: 已选择 {count} 个文件。
|
||||
lastModified: 最后修改
|
||||
move: 移动
|
||||
moveMessage: 请选择欲移动至的目录:
|
||||
newDir: 新建目录
|
||||
newDirMessage: 请输入新目录的名称。
|
||||
newFile: 新建文件
|
||||
newFileMessage: 请输入新文件的名称。
|
||||
numberDirs: 目录数
|
||||
numberFiles: 文件数
|
||||
replace: 替换
|
||||
replaceMessage: >
|
||||
您尝试上传的文件中有一个与现有文件的名称存在冲突。
|
||||
是否替换现有的同名文件?
|
||||
rename: 重命名
|
||||
renameMessage: 请输入新名称,旧名称为:
|
||||
show: 揭示
|
||||
size: 大小
|
||||
schedule: 计划
|
||||
scheduleMessage: 请选择发布这篇帖子的日期。
|
||||
newArchetype: 创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。
|
||||
settings:
|
||||
admin: 管理员
|
||||
administrator: 管理员
|
||||
allowCommands: 执行命令(Linux 代码)
|
||||
allowEdit: 编辑、重命名或删除文件/目录
|
||||
allowNew: 创建新文件和目录
|
||||
allowPublish: 发布新的帖子与页面
|
||||
avoidChanges: '(留空以避免更改)'
|
||||
changePassword: 更改密码
|
||||
commands: 命令(linux 代码)
|
||||
commandsHelp: >
|
||||
在这里,您可以设置在指定事件中执行的命令,一行一条。
|
||||
若事件与文件相关,如“在保存文件前”,
|
||||
则文件的路径会被赋值给环境变量 "file"。
|
||||
commandsUpdated: 命令已更新!
|
||||
customStylesheet: 自定义样式表
|
||||
examples: 例子
|
||||
globalSettings: 全局设置
|
||||
language: 语言
|
||||
newPassword: 您的新密码
|
||||
newPasswordConfirm: 重输一遍新密码
|
||||
newUser: 新建用户
|
||||
password: 密码
|
||||
passwordUpdated: 密码已更新!
|
||||
permissions: 权限
|
||||
permissionsHelp: >
|
||||
'您可以将该用户设置为管理员,也可以单独选择各项权限。
|
||||
如果选择了"管理员(Administrator)",则其他的选项会被自动勾上,
|
||||
同时该用户可以管理其他用户。'
|
||||
profileSettings: 配置文件设置
|
||||
ruleExample1: >
|
||||
'阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore)。'
|
||||
ruleExample2: 阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。
|
||||
rules: 规则
|
||||
rulesHelp1: >
|
||||
'您可以为该用户制定一组黑名单或白名单式的规则,
|
||||
被屏蔽的文件将不会显示在列表中, 用户也无权限访问,
|
||||
支持相对于目录范围的路径。'
|
||||
rulesHelp2: >
|
||||
每行一条规则,且必须以关键词 {0} 或 {1} 开头。
|
||||
如要使用正则表达式,请在加上 {2} 之后再附上表达式或路径。
|
||||
scope: 目录范围
|
||||
settingsUpdated: 设置已更新!
|
||||
user: 用户
|
||||
userCommands: 用户命令(Linux 代码)
|
||||
userCommandsHelp: >
|
||||
指定该用户可以执行的命令(Linux 代码),用空格分隔。
|
||||
例:
|
||||
userCreated: 用户已创建!
|
||||
userDeleted: 用户已删除!
|
||||
userManagement: 用户管理
|
||||
username: 用户名
|
||||
users: 用户
|
||||
userUpdated: 用户已更新!
|
||||
sidebar:
|
||||
help: 帮助
|
||||
logout: 登出
|
||||
myFiles: 我的文件
|
||||
newFile: 新建文件
|
||||
newFolder: 新建文件夹
|
||||
servedWith: '服务提供者:'
|
||||
settings: 设置
|
||||
siteSettings: 网站设置
|
||||
hugoNew: Hugo New
|
||||
preview: 预览
|
||||
search:
|
||||
images: 图像
|
||||
music: 音乐
|
||||
pdf: PDF
|
||||
pressToExecute: 按回车键执行。
|
||||
pressToSearch: 按回车键搜索。
|
||||
search: 搜索...
|
||||
searchOrCommand: 搜索或者执行命令(Linux 代码)...
|
||||
searchOrSupportedCommand: 搜索或使用您可以使用的命令(一次只能执行一个命令):
|
||||
type: 键入并按回车键进行搜索.
|
||||
types: 类型
|
||||
video: 视频
|
||||
writeToSearch: 请输入要搜索的内容
|
||||
languages:
|
||||
en: English
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
time:
|
||||
unit: 时间单位
|
||||
seconds: 秒
|
||||
minutes: 分钟
|
||||
hours: 小时
|
||||
days: 天
|
||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||
import App from './App'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
|
||||
Vue.config.productionTip = true
|
||||
|
||||
@@ -10,6 +11,7 @@ new Vue({
|
||||
el: '#app',
|
||||
store,
|
||||
router,
|
||||
i18n,
|
||||
template: '<App/>',
|
||||
components: { App }
|
||||
})
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Login from '@/components/Login'
|
||||
import Main from '@/components/Main'
|
||||
import Files from '@/components/Files'
|
||||
import Users from '@/components/Users'
|
||||
import User from '@/components/User'
|
||||
import GlobalSettings from '@/components/GlobalSettings'
|
||||
import ProfileSettings from '@/components/ProfileSettings'
|
||||
import error403 from '@/components/errors/403'
|
||||
import error404 from '@/components/errors/404'
|
||||
import error500 from '@/components/errors/500'
|
||||
import Login from '@/views/Login'
|
||||
import Layout from '@/views/Layout'
|
||||
import Files from '@/views/Files'
|
||||
import Users from '@/views/Users'
|
||||
import User from '@/views/User'
|
||||
import GlobalSettings from '@/views/GlobalSettings'
|
||||
import ProfileSettings from '@/views/ProfileSettings'
|
||||
import Error403 from '@/views/errors/403'
|
||||
import Error404 from '@/views/errors/404'
|
||||
import Error500 from '@/views/errors/500'
|
||||
import auth from '@/utils/auth.js'
|
||||
import store from '@/store'
|
||||
|
||||
@@ -25,24 +25,18 @@ const router = new Router({
|
||||
component: Login,
|
||||
beforeEnter: function (to, from, next) {
|
||||
auth.loggedIn()
|
||||
.then(() => {
|
||||
next({ path: '/files' })
|
||||
})
|
||||
.catch(() => {
|
||||
document.title = 'Login'
|
||||
next()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: {
|
||||
path: '/files/'
|
||||
.then(() => {
|
||||
next({ path: '/files' })
|
||||
})
|
||||
.catch(() => {
|
||||
document.title = 'Login'
|
||||
next()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/*',
|
||||
component: Main,
|
||||
component: Layout,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
},
|
||||
@@ -75,17 +69,17 @@ const router = new Router({
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: error403
|
||||
component: Error403
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: 'Not Found',
|
||||
component: error404
|
||||
component: Error404
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: 'Internal Server Error',
|
||||
component: error500
|
||||
component: Error500
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
@@ -95,12 +89,6 @@ const router = new Router({
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/users/',
|
||||
redirect: {
|
||||
path: '/users'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/users/*',
|
||||
name: 'User',
|
||||
@@ -109,6 +97,12 @@ const router = new Router({
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/files',
|
||||
redirect: {
|
||||
path: '/files/'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/*',
|
||||
redirect: {
|
||||
|
||||
@@ -8,19 +8,21 @@ Vue.use(Vuex)
|
||||
const state = {
|
||||
user: {},
|
||||
req: {},
|
||||
plugins: window.plugins || [],
|
||||
clipboard: {
|
||||
key: '',
|
||||
items: []
|
||||
},
|
||||
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
|
||||
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
|
||||
jwt: '',
|
||||
schedule: '',
|
||||
loading: false,
|
||||
reload: false,
|
||||
selected: [],
|
||||
multiple: false,
|
||||
show: null,
|
||||
showMessage: null
|
||||
showMessage: null,
|
||||
showConfirm: null
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import i18n from '@/i18n'
|
||||
import moment from 'moment'
|
||||
|
||||
const mutations = {
|
||||
closeHovers: state => {
|
||||
state.show = null
|
||||
@@ -11,6 +14,7 @@ const mutations = {
|
||||
|
||||
state.show = value.prompt
|
||||
state.showMessage = value.message
|
||||
state.showConfirm = value.confirm
|
||||
},
|
||||
showError: (state, value) => {
|
||||
state.show = 'error'
|
||||
@@ -22,11 +26,17 @@ const mutations = {
|
||||
},
|
||||
setLoading: (state, value) => { state.loading = value },
|
||||
setReload: (state, value) => { state.reload = value },
|
||||
setUser: (state, value) => (state.user = value),
|
||||
setUserCSS: (state, value) => (state.user.css = value),
|
||||
setUser: (state, value) => {
|
||||
moment.locale(value.locale)
|
||||
i18n.locale = value.locale
|
||||
state.user = value
|
||||
},
|
||||
setJWT: (state, value) => (state.jwt = value),
|
||||
multiple: (state, value) => (state.multiple = value),
|
||||
addSelected: (state, value) => (state.selected.push(value)),
|
||||
addPlugin: (state, value) => {
|
||||
state.plugins.push(value)
|
||||
},
|
||||
removeSelected: (state, value) => {
|
||||
let i = state.selected.indexOf(value)
|
||||
if (i === -1) return
|
||||
@@ -48,6 +58,9 @@ const mutations = {
|
||||
resetClipboard: (state) => {
|
||||
state.clipboard.key = ''
|
||||
state.clipboard.items = []
|
||||
},
|
||||
setSchedule: (state, value) => {
|
||||
state.schedule = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,17 @@ import store from '@/store'
|
||||
|
||||
const ssl = (window.location.protocol === 'https:')
|
||||
|
||||
function removePrefix (url) {
|
||||
export function removePrefix (url) {
|
||||
if (url.startsWith('/files')) {
|
||||
return url.slice(6)
|
||||
url = url.slice(6)
|
||||
}
|
||||
|
||||
if (url === '') url = '/'
|
||||
if (url[0] !== '/') url = '/' + url
|
||||
return url
|
||||
}
|
||||
|
||||
function fetch (url) {
|
||||
export function fetch (url) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -24,10 +26,7 @@ function fetch (url) {
|
||||
resolve(JSON.parse(request.responseText))
|
||||
break
|
||||
default:
|
||||
reject({
|
||||
message: request.responseText,
|
||||
status: request.status
|
||||
})
|
||||
reject(new Error(request.status))
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -36,7 +35,7 @@ function fetch (url) {
|
||||
})
|
||||
}
|
||||
|
||||
function rm (url) {
|
||||
export function remove (url) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -57,7 +56,7 @@ function rm (url) {
|
||||
})
|
||||
}
|
||||
|
||||
function post (url, content = '') {
|
||||
export function post (url, content = '', overwrite = false) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -65,26 +64,39 @@ function post (url, content = '') {
|
||||
request.open('POST', `${store.state.baseURL}/api/resource${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
if (overwrite) {
|
||||
request.setRequestHeader('Action', `override`)
|
||||
}
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.responseText)
|
||||
} else if (request.status === 409) {
|
||||
reject(request.status)
|
||||
} else {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = (error) => reject(error)
|
||||
request.onerror = (error) => {
|
||||
reject(error)
|
||||
}
|
||||
request.send(content)
|
||||
})
|
||||
}
|
||||
|
||||
function put (url, content = '') {
|
||||
export function put (url, content = '', publish = false, date = '') {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
request.setRequestHeader('Publish', publish)
|
||||
|
||||
if (date !== '') {
|
||||
request.setRequestHeader('Schedule', date)
|
||||
}
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -132,15 +144,15 @@ function moveCopy (items, copy = false) {
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
function move (items) {
|
||||
export function move (items) {
|
||||
return moveCopy(items)
|
||||
}
|
||||
|
||||
function copy (items) {
|
||||
export function copy (items) {
|
||||
return moveCopy(items, true)
|
||||
}
|
||||
|
||||
function checksum (url, algo) {
|
||||
export function checksum (url, algo) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -160,10 +172,10 @@ function checksum (url, algo) {
|
||||
})
|
||||
}
|
||||
|
||||
function command (url, command, onmessage, onclose) {
|
||||
export function command (url, command, onmessage, onclose) {
|
||||
let protocol = (ssl ? 'wss:' : 'ws:')
|
||||
url = removePrefix(url)
|
||||
url = `${protocol}//${window.location.hostname}${store.state.baseURL}/api/command${url}`
|
||||
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/command${url}`
|
||||
|
||||
let conn = new window.WebSocket(url)
|
||||
conn.onopen = () => conn.send(command)
|
||||
@@ -171,10 +183,10 @@ function command (url, command, onmessage, onclose) {
|
||||
conn.onclose = onclose
|
||||
}
|
||||
|
||||
function search (url, search, onmessage, onclose) {
|
||||
export function search (url, search, onmessage, onclose) {
|
||||
let protocol = (ssl ? 'wss:' : 'ws:')
|
||||
url = removePrefix(url)
|
||||
url = `${protocol}//${window.location.hostname}${store.state.baseURL}/api/search${url}`
|
||||
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/search${url}`
|
||||
|
||||
let conn = new window.WebSocket(url)
|
||||
conn.onopen = () => conn.send(search)
|
||||
@@ -182,7 +194,7 @@ function search (url, search, onmessage, onclose) {
|
||||
conn.onclose = onclose
|
||||
}
|
||||
|
||||
function download (format, ...files) {
|
||||
export function download (format, ...files) {
|
||||
let url = `${store.state.baseURL}/api/download`
|
||||
|
||||
if (files.length === 1) {
|
||||
@@ -206,7 +218,59 @@ function download (format, ...files) {
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
function getUsers () {
|
||||
export function getSettings () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/settings/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve(JSON.parse(request.responseText))
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
export function updateSettings (param, which) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = {
|
||||
what: 'settings',
|
||||
which: which,
|
||||
data: {}
|
||||
}
|
||||
|
||||
data.data[which] = param
|
||||
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => { reject(error) }
|
||||
request.send(JSON.stringify(data))
|
||||
})
|
||||
}
|
||||
|
||||
// USERS
|
||||
|
||||
export function getUsers () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/users/`, true)
|
||||
@@ -227,7 +291,7 @@ function getUsers () {
|
||||
})
|
||||
}
|
||||
|
||||
function getUser (id) {
|
||||
export function getUser (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
|
||||
@@ -248,7 +312,7 @@ function getUser (id) {
|
||||
})
|
||||
}
|
||||
|
||||
function newUser (user) {
|
||||
export function newUser (user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${store.state.baseURL}/api/users/`, true)
|
||||
@@ -265,11 +329,15 @@ function newUser (user) {
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify(user))
|
||||
request.send(JSON.stringify({
|
||||
what: 'user',
|
||||
which: 'new',
|
||||
data: user
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
export function updateUser (user, which) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
|
||||
@@ -286,11 +354,15 @@ function updateUser (user) {
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify(user))
|
||||
request.send(JSON.stringify({
|
||||
what: 'user',
|
||||
which: (typeof which === 'string') ? which : 'all',
|
||||
data: user
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function deleteUser (id) {
|
||||
export function deleteUser (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
|
||||
@@ -311,153 +383,69 @@ function deleteUser (id) {
|
||||
})
|
||||
}
|
||||
|
||||
function updatePassword (password) {
|
||||
// SHARE
|
||||
|
||||
export function getShare (url) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/users/change-password`, true)
|
||||
request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
if (request.status === 200) {
|
||||
resolve(JSON.parse(request.responseText))
|
||||
} else {
|
||||
reject(request.status)
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify({ 'password': password }))
|
||||
})
|
||||
}
|
||||
|
||||
function updateCSS (css) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/users/change-css`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify({ 'css': css }))
|
||||
})
|
||||
}
|
||||
|
||||
function getCommands () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/commands/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve(JSON.parse(request.responseText))
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
function updateCommands (commands) {
|
||||
export function deleteShare (hash) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/commands/`, true)
|
||||
request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
if (request.status === 200) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(request.status)
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify(commands))
|
||||
})
|
||||
}
|
||||
|
||||
function getPlugins () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/plugins/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve(JSON.parse(request.responseText))
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
function updatePlugins (data) {
|
||||
export function share (url, expires = '', unit = 'hours') {
|
||||
url = removePrefix(url)
|
||||
url = `${store.state.baseURL}/api/share${url}`
|
||||
if (expires !== '') {
|
||||
url += `?expires=${expires}&unit=${unit}`
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/plugins/`, true)
|
||||
request.open('POST', url, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
if (request.status === 200) {
|
||||
resolve(JSON.parse(request.responseText))
|
||||
} else {
|
||||
reject(request.responseStatus)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify(data))
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
delete: rm,
|
||||
fetch,
|
||||
checksum,
|
||||
move,
|
||||
put,
|
||||
copy,
|
||||
post,
|
||||
command,
|
||||
search,
|
||||
download,
|
||||
getUser,
|
||||
newUser,
|
||||
updateUser,
|
||||
getUsers,
|
||||
updatePassword,
|
||||
updateCSS,
|
||||
getCommands,
|
||||
updateCommands,
|
||||
removePrefix,
|
||||
getPlugins,
|
||||
updatePlugins,
|
||||
deleteUser
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ function loggedIn () {
|
||||
parseToken(request.responseText)
|
||||
resolve()
|
||||
} else {
|
||||
reject()
|
||||
reject(new Error(request.responseText))
|
||||
}
|
||||
}
|
||||
request.onerror = () => reject()
|
||||
request.onerror = () => reject(new Error('Could not finish the request'))
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
@@ -45,7 +45,7 @@ function login (user, password) {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
request.onerror = () => reject()
|
||||
request.onerror = () => reject(new Error('Could not finish the request'))
|
||||
request.send(JSON.stringify(data))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ function loading (button) {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function done (button, success = true) {
|
||||
function done (button) {
|
||||
let el = document.querySelector(`#${button}-button > i`)
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
@@ -33,7 +33,34 @@ function done (button, success = true) {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function success (button) {
|
||||
let el = document.querySelector(`#${button}-button > i`)
|
||||
|
||||
if (el === undefined || el === null) {
|
||||
console.log('Error getting button ' + button)
|
||||
return
|
||||
}
|
||||
|
||||
el.style.opacity = 0
|
||||
|
||||
setTimeout(() => {
|
||||
el.classList.remove('spin')
|
||||
el.innerHTML = 'done'
|
||||
el.style.opacity = 1
|
||||
|
||||
setTimeout(() => {
|
||||
el.style.opacity = 0
|
||||
|
||||
setTimeout(() => {
|
||||
el.innerHTML = el.dataset.icon
|
||||
el.style.opacity = 1
|
||||
}, 100)
|
||||
}, 500)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
export default {
|
||||
loading,
|
||||
done
|
||||
done,
|
||||
success
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="breadcrumbs">
|
||||
<router-link to="/files/">
|
||||
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||
<i class="material-icons">home</i>
|
||||
</router-link>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="error">
|
||||
<not-found v-if="error === 404"></not-found>
|
||||
<forbidden v-else-if="error === 403"></forbidden>
|
||||
<not-found v-if="error.message === '404'"></not-found>
|
||||
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||
<internal-error v-else></internal-error>
|
||||
</div>
|
||||
<editor v-else-if="isEditor"></editor>
|
||||
@@ -20,7 +20,7 @@
|
||||
<preview v-else-if="isPreview"></preview>
|
||||
<div v-else>
|
||||
<h2 class="message">
|
||||
<span>Loading...</span>
|
||||
<span>{{ $t('files.loading') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,10 +30,10 @@
|
||||
import Forbidden from './errors/403'
|
||||
import NotFound from './errors/404'
|
||||
import InternalError from './errors/500'
|
||||
import Preview from './Preview'
|
||||
import Listing from './Listing'
|
||||
import Editor from './Editor'
|
||||
import api from '@/utils/api'
|
||||
import Preview from '@/components/files/Preview'
|
||||
import Listing from '@/components/files/Listing'
|
||||
import Editor from '@/components/files/Editor'
|
||||
import * as api from '@/utils/api'
|
||||
import { mapGetters, mapState, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
@@ -116,20 +116,14 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
window.addEventListener('scroll', event => {
|
||||
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
|
||||
|
||||
let top = 112 - window.scrollY
|
||||
|
||||
if (top < 64) {
|
||||
top = 64
|
||||
}
|
||||
|
||||
document.querySelector('#listing.list .item.header').style.top = top + 'px'
|
||||
})
|
||||
window.addEventListener('scroll', this.scroll)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
window.removeEventListener('scroll', this.scroll)
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.commit('updateRequest', {})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'setLoading' ]),
|
||||
@@ -149,25 +143,19 @@ export default {
|
||||
if (url[0] !== '/') url = '/' + url
|
||||
|
||||
api.fetch(url)
|
||||
.then((req) => {
|
||||
if (!url.endsWith('/') && req.url.endsWith('/')) {
|
||||
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
|
||||
}
|
||||
.then((req) => {
|
||||
if (!url.endsWith('/') && req.url.endsWith('/')) {
|
||||
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
|
||||
}
|
||||
|
||||
this.$store.commit('updateRequest', req)
|
||||
document.title = req.name
|
||||
this.setLoading(false)
|
||||
})
|
||||
.catch(error => {
|
||||
this.setLoading(false)
|
||||
|
||||
if (typeof error === 'object') {
|
||||
this.error = error.status
|
||||
return
|
||||
}
|
||||
|
||||
this.error = error
|
||||
})
|
||||
this.$store.commit('updateRequest', req)
|
||||
document.title = req.name
|
||||
this.setLoading(false)
|
||||
})
|
||||
.catch(error => {
|
||||
this.setLoading(false)
|
||||
this.error = error
|
||||
})
|
||||
},
|
||||
keyEvent (event) {
|
||||
// Esc!
|
||||
@@ -217,11 +205,21 @@ export default {
|
||||
|
||||
if (this.req.kind !== 'editor') {
|
||||
document.getElementById('download-button').click()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scroll (event) {
|
||||
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
|
||||
|
||||
let top = 112 - window.scrollY
|
||||
|
||||
if (top < 64) {
|
||||
top = 64
|
||||
}
|
||||
|
||||
document.querySelector('#listing.list .item.header').style.top = top + 'px'
|
||||
},
|
||||
openSidebar () {
|
||||
this.$store.commit('showHover', 'sidebar')
|
||||
},
|
||||
162
assets/src/views/GlobalSettings.vue
Normal file
162
assets/src/views/GlobalSettings.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<ul id="nav">
|
||||
<li>
|
||||
<router-link to="/settings/profile">
|
||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.profileSettings') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/users">
|
||||
{{ $t('settings.userManagement') }} <i class="material-icons">keyboard_arrow_right</i>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h1>{{ $t('settings.globalSettings') }}</h1>
|
||||
|
||||
<form @submit="saveStaticGen" v-if="$store.state.staticGen.length > 0">
|
||||
<h2>{{ capitalize($store.state.staticGen) }}</h2>
|
||||
|
||||
<p v-for="field in staticGen" :key="field.variable">
|
||||
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
|
||||
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
|
||||
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
|
||||
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
|
||||
</p>
|
||||
|
||||
<p><input type="submit" value="Save"></p>
|
||||
</form>
|
||||
|
||||
<form @submit="saveCommands">
|
||||
<h2>{{ $t('settings.commands') }}</h2>
|
||||
|
||||
<p class="small">{{ $t('settings.commandsHelp') }}</p>
|
||||
|
||||
<template v-for="command in commands">
|
||||
<h3>{{ capitalize(command.name) }}</h3>
|
||||
<textarea v-model.trim="command.value"></textarea>
|
||||
</template>
|
||||
|
||||
<p><input type="submit" value="Save"></p>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { getSettings, updateSettings } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
data: function () {
|
||||
return {
|
||||
commands: [],
|
||||
staticGen: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ])
|
||||
},
|
||||
created () {
|
||||
getSettings()
|
||||
.then(settings => {
|
||||
if (this.$store.state.staticGen.length > 0) {
|
||||
this.parseStaticGen(settings.staticGen)
|
||||
}
|
||||
|
||||
for (let key in settings.commands) {
|
||||
this.commands.push({
|
||||
name: key,
|
||||
value: settings.commands[key].join('\n')
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showSuccess', 'showError' ]),
|
||||
capitalize (name, where = '_') {
|
||||
if (where === 'caps') where = /(?=[A-Z])/
|
||||
let splitted = name.split(where)
|
||||
name = ''
|
||||
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
||||
}
|
||||
|
||||
return name.slice(0, -1)
|
||||
},
|
||||
saveCommands (event) {
|
||||
event.preventDefault()
|
||||
|
||||
let commands = {}
|
||||
|
||||
for (let command of this.commands) {
|
||||
let value = command.value.split('\n')
|
||||
if (value.length === 1 && value[0] === '') {
|
||||
value = []
|
||||
}
|
||||
|
||||
commands[command.name] = value
|
||||
}
|
||||
|
||||
updateSettings(commands, 'commands')
|
||||
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
saveStaticGen (event) {
|
||||
event.preventDefault()
|
||||
let staticGen = {}
|
||||
|
||||
for (let field of this.staticGen) {
|
||||
staticGen[field.variable] = field.value
|
||||
|
||||
if (field.original === 'array') {
|
||||
let val = field.value.split(' ')
|
||||
if (val[0] === '') {
|
||||
val.shift()
|
||||
}
|
||||
|
||||
staticGen[field.variable] = val
|
||||
}
|
||||
}
|
||||
|
||||
updateSettings(staticGen, 'staticGen')
|
||||
.then(() => { this.showSuccess(this.$t('settings.settingsUpdated')) })
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
parseStaticGen (staticgen) {
|
||||
for (let option of staticgen) {
|
||||
let value = option.value
|
||||
|
||||
let field = {
|
||||
name: option.name,
|
||||
variable: option.variable,
|
||||
type: 'text',
|
||||
original: 'text',
|
||||
value: value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
field.original = 'array'
|
||||
field.value = value.join(' ')
|
||||
|
||||
this.staticGen.push(field)
|
||||
continue
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
field.type = 'checkbox'
|
||||
field.original = 'boolean'
|
||||
break
|
||||
}
|
||||
|
||||
this.staticGen.push(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -10,13 +10,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from './Search'
|
||||
import Sidebar from './Sidebar'
|
||||
import Prompts from './prompts/Prompts'
|
||||
import SiteHeader from './Header'
|
||||
import Search from '@/components/Search'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import Prompts from '@/components/prompts/Prompts'
|
||||
import SiteHeader from '@/components/Header'
|
||||
|
||||
export default {
|
||||
name: 'main',
|
||||
name: 'layout',
|
||||
components: {
|
||||
Search,
|
||||
Sidebar,
|
||||
42
assets/src/views/Login.vue
Normal file
42
assets/src/views/Login.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div id="login">
|
||||
<form @submit="submit">
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<h1>File Manager</h1>
|
||||
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
|
||||
<input type="text" v-model="username" :placeholder="$t('login.username')">
|
||||
<input type="password" v-model="password" :placeholder="$t('login.password')">
|
||||
<input type="submit" :value="$t('login.submit')">
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
data: function () {
|
||||
return {
|
||||
wrong: false,
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
let redirect = this.$route.query.redirect
|
||||
if (redirect === '' || redirect === undefined || redirect === null) {
|
||||
redirect = '/files/'
|
||||
}
|
||||
|
||||
auth.login(this.username, this.password)
|
||||
.then(() => { this.$router.push({ path: redirect }) })
|
||||
.catch(() => { this.wrong = true })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
103
assets/src/views/ProfileSettings.vue
Normal file
103
assets/src/views/ProfileSettings.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<ul id="nav" v-if="user.admin">
|
||||
<li>
|
||||
<router-link to="/settings/global">
|
||||
{{ $t('settings.globalSettings') }} <i class="material-icons">keyboard_arrow_right</i>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h1>{{ $t('settings.profileSettings') }}</h1>
|
||||
|
||||
<form @submit="updateSettings">
|
||||
<h3>{{ $t('settings.language') }}</h3>
|
||||
<p><languages id="locale" :selected.sync="locale"></languages></p>
|
||||
<h3>{{ $t('settings.customStylesheet') }}</h3>
|
||||
<textarea v-model="css" name="css"></textarea>
|
||||
<p><input type="submit" :value="$t('buttons.update')"></p>
|
||||
</form>
|
||||
|
||||
<form @submit="updatePassword">
|
||||
<h3>{{ $t('settings.changePassword') }}</h3>
|
||||
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p>
|
||||
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p>
|
||||
<p><input type="submit" :value="$t('buttons.update')"></p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { updateUser } from '@/utils/api'
|
||||
import Languages from '@/components/Languages'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
components: {
|
||||
Languages
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
password: '',
|
||||
passwordConf: '',
|
||||
css: '',
|
||||
locale: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ]),
|
||||
passwordClass () {
|
||||
if (this.password === '' && this.passwordConf === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (this.password === this.passwordConf) {
|
||||
return 'green'
|
||||
}
|
||||
|
||||
return 'red'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.css = this.user.css
|
||||
this.locale = this.user.locale
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showSuccess' ]),
|
||||
updatePassword (event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (this.password !== this.passwordConf) {
|
||||
return
|
||||
}
|
||||
|
||||
let user = {
|
||||
ID: this.$store.state.user.ID,
|
||||
password: this.password
|
||||
}
|
||||
|
||||
updateUser(user, 'password').then(location => {
|
||||
this.showSuccess(this.$t('settings.passwordUpdated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
},
|
||||
updateSettings (event) {
|
||||
event.preventDefault()
|
||||
|
||||
let user = {...this.$store.state.user}
|
||||
user.css = this.css
|
||||
user.locale = this.locale
|
||||
|
||||
updateUser(user, 'partial').then(location => {
|
||||
this.$store.commit('setUser', user)
|
||||
this.$emit('css-updated')
|
||||
this.showSuccess(this.$t('settings.settingsUpdated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,58 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="save" class="dashboard">
|
||||
<h1 v-if="id === 0">New User</h1>
|
||||
<h1 v-else>User {{ username }}</h1>
|
||||
<ul id="nav">
|
||||
<li>
|
||||
<router-link to="/users">
|
||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.userManagement') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li></li>
|
||||
</ul>
|
||||
|
||||
<p><label for="username">Username</label><input type="text" v-model="username" id="username"></p>
|
||||
<p><label for="password">Password</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
|
||||
<p><label for="scope">Scope</label><input type="text" v-model="filesystem" id="scope"></p>
|
||||
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
|
||||
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
|
||||
|
||||
<h2>Permissions</h2>
|
||||
|
||||
<p class="small">You can set the user to be an administrator or choose the permissions individually.
|
||||
If you select "Administrator", all of the other options will be automatically checked.
|
||||
The management of users remains a privilege of an administrator.</p>
|
||||
|
||||
<p><input type="checkbox" v-model="admin"> Administrator</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> Create new files and directories</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> Edit, rename and delete files or directories.</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> Execute commands</p>
|
||||
<p v-for="(value, key) in permissions" :key="key">
|
||||
<input type="checkbox" :disabled="admin" v-model="permissions[key]"> {{ capitalize(key) }}
|
||||
<p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></p>
|
||||
<p><label for="password">{{ $t('settings.password') }}</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
|
||||
<p><label for="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p>
|
||||
<p>
|
||||
<label for="locale">{{ $t('settings.language') }}</label>
|
||||
<languages id="locale" :selected.sync="locale"></languages>
|
||||
</p>
|
||||
|
||||
<h3>Commands</h3>
|
||||
<h2>{{ $t('settings.permissions') }}</h2>
|
||||
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
|
||||
|
||||
<p class="small">A space separated list with the available commands for this user. Example: <i>git svn hg</i>.</p>
|
||||
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
|
||||
<p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
|
||||
|
||||
<h3>{{ $t('settings.userCommands') }}</h3>
|
||||
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
|
||||
<input type="text" v-model.trim="commands">
|
||||
|
||||
<h2>Rules</h2>
|
||||
<h2>{{ $t('settings.rules') }}</h2>
|
||||
|
||||
<p class="small">Here you can define a set of allow and disallow rules for this specific user. The blocked files won't
|
||||
show up in the listings and they won't be accessible to the user. We support regex and paths relative to
|
||||
the user's scope.</p>
|
||||
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
|
||||
|
||||
<p class="small">Each rule goes in one different line and must start with the keyword <code>allow</code> or <code>disallow</code>.
|
||||
Then you should write <code>regex</code> if you are using a regular expression and then the expression or the path.</p>
|
||||
<i18n path="settings.rulesHelp2" tag="p" class="small">
|
||||
<code>allow</code><code>disallow</code><code>regex</code>
|
||||
</i18n>
|
||||
|
||||
<p class="small"><strong>Examples</strong></p>
|
||||
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
|
||||
|
||||
<ul class="small">
|
||||
<li><code>disallow regex \\/\\..+</code> - prevents the access to any dot file (such as .git, .gitignore) in every folder.</li>
|
||||
<li><code>disallow /Caddyfile</code> - blocks the access to the file named <i>Caddyfile</i> on the root of the scope</li>
|
||||
<li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
|
||||
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
|
||||
</ul>
|
||||
|
||||
<textarea v-model.trim="rules"></textarea>
|
||||
|
||||
<h2>Custom Stylesheet</h2>
|
||||
<h2>{{ $t('settings.customStylesheet') }}</h2>
|
||||
|
||||
<textarea name="css"></textarea>
|
||||
|
||||
<p>
|
||||
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete">Delete</button>
|
||||
<input type="submit" value="Save">
|
||||
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<input type="submit" :value="$t('buttons.save')">
|
||||
</p>
|
||||
</form>
|
||||
|
||||
@@ -60,8 +65,13 @@
|
||||
<h3>Delete User</h3>
|
||||
<p>Are you sure you want to delete this user?</p>
|
||||
<div>
|
||||
<button @click="deleteUser" autofocus>Delete</button>
|
||||
<button @click="closeHovers" class="cancel">Cancel</button>
|
||||
<button @click="deleteUser" autofocus>{{ $t('buttons.delete') }}</button>
|
||||
<button class="cancel"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">
|
||||
{{ $t('buttons.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,10 +79,12 @@
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
|
||||
import Languages from '@/components/Languages'
|
||||
|
||||
export default {
|
||||
name: 'user',
|
||||
components: { Languages },
|
||||
data: () => {
|
||||
return {
|
||||
id: 0,
|
||||
@@ -80,11 +92,13 @@ export default {
|
||||
allowNew: false,
|
||||
allowEdit: false,
|
||||
allowCommands: false,
|
||||
allowPublish: false,
|
||||
permissions: {},
|
||||
password: '',
|
||||
username: '',
|
||||
filesystem: '',
|
||||
rules: '',
|
||||
locale: '',
|
||||
css: '',
|
||||
commands: ''
|
||||
}
|
||||
@@ -92,7 +106,7 @@ export default {
|
||||
computed: {
|
||||
passwordPlaceholder () {
|
||||
if (this.$route.path === '/users/new') return ''
|
||||
return '(leave blank to avoid changes)'
|
||||
return this.$t('settings.avoidChanges')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
@@ -105,6 +119,7 @@ export default {
|
||||
this.allowCommands = true
|
||||
this.allowEdit = true
|
||||
this.allowNew = true
|
||||
this.allowPublish = true
|
||||
for (let key in this.permissions) {
|
||||
this.permissions[key] = true
|
||||
}
|
||||
@@ -119,17 +134,19 @@ export default {
|
||||
user = 'base'
|
||||
}
|
||||
|
||||
api.getUser(user).then(user => {
|
||||
getUser(user).then(user => {
|
||||
this.id = user.ID
|
||||
this.admin = user.admin
|
||||
this.allowCommands = user.allowCommands
|
||||
this.allowNew = user.allowNew
|
||||
this.allowEdit = user.allowEdit
|
||||
this.allowPublish = user.allowPublish
|
||||
this.filesystem = user.filesystem
|
||||
this.username = user.username
|
||||
this.commands = user.commands.join(' ')
|
||||
this.css = user.css
|
||||
this.permissions = user.permissions
|
||||
this.locale = user.locale
|
||||
|
||||
for (let rule of user.rules) {
|
||||
if (rule.allow) {
|
||||
@@ -167,12 +184,14 @@ export default {
|
||||
this.admin = false
|
||||
this.allowNew = false
|
||||
this.allowEdit = false
|
||||
this.allowPublish = false
|
||||
this.permissins = {}
|
||||
this.allowCommands = false
|
||||
this.password = ''
|
||||
this.username = ''
|
||||
this.filesystem = ''
|
||||
this.rules = ''
|
||||
this.locale = ''
|
||||
this.css = ''
|
||||
this.commands = ''
|
||||
},
|
||||
@@ -182,9 +201,9 @@ export default {
|
||||
deleteUser (event) {
|
||||
event.preventDefault()
|
||||
|
||||
api.deleteUser(this.id).then(location => {
|
||||
deleteUser(this.id).then(location => {
|
||||
this.$router.push({ path: '/users' })
|
||||
this.$store.commit('showSuccess', 'User deleted!')
|
||||
this.$store.commit('showSuccess', this.$t('settings.userDeleted'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
@@ -194,9 +213,9 @@ export default {
|
||||
let user = this.parseForm()
|
||||
|
||||
if (this.$route.path === '/users/new') {
|
||||
api.newUser(user).then(location => {
|
||||
newUser(user).then(location => {
|
||||
this.$router.push({ path: location })
|
||||
this.$store.commit('showSuccess', 'User created!')
|
||||
this.$store.commit('showSuccess', this.$t('settings.userCreated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
@@ -204,8 +223,12 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
api.updateUser(user).then(location => {
|
||||
this.$store.commit('showSuccess', 'User updated!')
|
||||
updateUser(user).then(location => {
|
||||
if (user.ID === this.$store.state.user.ID) {
|
||||
this.$store.commit('setUser', user)
|
||||
}
|
||||
|
||||
this.$store.commit('showSuccess', this.$t('settings.userUpdated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
@@ -220,8 +243,10 @@ export default {
|
||||
allowCommands: this.allowCommands,
|
||||
allowNew: this.allowNew,
|
||||
allowEdit: this.allowEdit,
|
||||
allowPublish: this.allowPublish,
|
||||
permissions: this.permissions,
|
||||
css: this.css,
|
||||
locale: this.locale,
|
||||
commands: this.commands.split(' '),
|
||||
rules: []
|
||||
}
|
||||
@@ -269,7 +294,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,12 +1,21 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>Users <router-link to="/users/new"><button>New</button></router-link></h1>
|
||||
<ul id="nav">
|
||||
<li>
|
||||
<router-link to="/settings/global">
|
||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.globalSettings') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li></li>
|
||||
</ul>
|
||||
|
||||
<h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Admin</th>
|
||||
<th>Scope</th>
|
||||
<th>{{ $t('settings.username') }}</th>
|
||||
<th>{{ $t('settings.admin') }}</th>
|
||||
<th>{{ $t('settings.scope') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
@@ -22,7 +31,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'users',
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">error</i>
|
||||
<span>You're not welcome here.</span>
|
||||
<span>{{ $t('errors.forbidden') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">gps_off</i>
|
||||
<span>This location can't be reached.</span>
|
||||
<span>{{ $t('errors.notFound') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">error_outline</i>
|
||||
<span>Something really went wrong.</span>
|
||||
<span>{{ $t('errors.internal') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
50
assets/static/share/404.html
Normal file
50
assets/static/share/404.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<title>File Manager</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
|
||||
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
|
||||
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
|
||||
<meta name="theme-color" content="#2979ff">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="assets">
|
||||
<link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
|
||||
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
|
||||
<meta name="msapplication-TileColor" content="#2979ff">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #6f6f6f;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
body > div {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
background: #fff;
|
||||
display: block;
|
||||
border-radius: 0.2em;
|
||||
padding: 2em 3em;
|
||||
}
|
||||
body > a * {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div><h1>404 Not Found</h1></div>
|
||||
</body>
|
||||
</html>
|
||||
85
assets/static/share/index.html
Normal file
85
assets/static/share/index.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<title>{{ .File.Name }}</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
|
||||
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
|
||||
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
|
||||
<meta name="theme-color" content="#2979ff">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="assets">
|
||||
<link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
|
||||
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
|
||||
<meta name="msapplication-TileColor" content="#2979ff">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #6f6f6f;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
body > a {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
background: #fff;
|
||||
display: block;
|
||||
border-radius: 0.2em;
|
||||
width: 90%;
|
||||
max-width: 25em;
|
||||
}
|
||||
body > a > div:first-child {
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
background: #ffffff;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
body > a > div:last-child {
|
||||
padding: 2em 3em;
|
||||
}
|
||||
body > a * {
|
||||
margin: 0;
|
||||
}
|
||||
body > a h1 {
|
||||
margin-top: .2em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="?dl=1">
|
||||
<div>Download {{ if .File.IsDir }}Folder{{ else }}File{{ end }}</div>
|
||||
<div>
|
||||
{{ if .File.IsDir -}}
|
||||
<svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
{{ else -}}
|
||||
<svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
{{ end -}}
|
||||
<h1>{{ .File.Name }}</h1>
|
||||
</div>
|
||||
</a>
|
||||
</body>
|
||||
</html>
|
||||
23
auth.go
23
auth.go
@@ -15,6 +15,11 @@ import (
|
||||
|
||||
// authHandler proccesses the authentication for the user.
|
||||
func authHandler(c *RequestContext, 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
|
||||
if r.Body == nil {
|
||||
@@ -27,7 +32,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
|
||||
}
|
||||
|
||||
// Checks if the user exists.
|
||||
u, ok := c.FM.Users[cred.Username]
|
||||
u, ok := c.Users[cred.Username]
|
||||
if !ok {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -56,6 +61,7 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
// claims is the JWT claims.
|
||||
type claims struct {
|
||||
User
|
||||
NoAuth bool `json:"noAuth"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
@@ -70,6 +76,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
|
||||
// Builds the claims.
|
||||
claims := claims{
|
||||
u,
|
||||
c.NoAuth,
|
||||
jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
|
||||
Issuer: "File Manager",
|
||||
@@ -78,14 +85,15 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
|
||||
|
||||
// Creates the token and signs it.
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
string, err := token.SignedString(c.FM.key)
|
||||
signed, err := token.SignedString(c.key)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Writes the token.
|
||||
w.Write([]byte(string))
|
||||
w.Header().Set("Content-Type", "cty")
|
||||
w.Write([]byte(signed))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -112,8 +120,13 @@ 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) {
|
||||
if c.NoAuth {
|
||||
c.User = c.DefaultUser
|
||||
return true, c.User
|
||||
}
|
||||
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return c.FM.key, nil
|
||||
return c.key, nil
|
||||
}
|
||||
var claims claims
|
||||
token, err := request.ParseFromRequestWithClaims(r,
|
||||
@@ -126,7 +139,7 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
u, ok := c.FM.Users[claims.User.Username]
|
||||
u, ok := c.Users[claims.User.Username]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
11
build.sh
11
build.sh
@@ -1,6 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Install rice tool if not present
|
||||
if ! [ -x "$(command -v rice)" ]; then
|
||||
go get github.com/GeertJohan/go.rice/rice
|
||||
fi
|
||||
|
||||
# Clean the dist folder and build the assets
|
||||
rm -rf assets/dist
|
||||
npm run build
|
||||
rice embed-go
|
||||
cd ./caddy/hugo
|
||||
|
||||
# Embed the assets using rice
|
||||
rice embed-go
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
. "github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/filemanager/dir"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
@@ -73,6 +74,7 @@ func parse(c *caddy.Controller) ([]*config, error) {
|
||||
baseURL := "/"
|
||||
baseScope := "."
|
||||
database := ""
|
||||
noAuth := false
|
||||
|
||||
// Get the baseURL and baseScope
|
||||
args := c.RemainingArgs()
|
||||
@@ -93,6 +95,17 @@ func parse(c *caddy.Controller) ([]*config, error) {
|
||||
}
|
||||
|
||||
database = c.Val()
|
||||
case "no_auth":
|
||||
if !c.NextArg() {
|
||||
noAuth = true
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
noAuth, err = strconv.ParseBool(c.Val())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +139,7 @@ func parse(c *caddy.Controller) ([]*config, error) {
|
||||
}
|
||||
|
||||
fm, err := New(database, User{
|
||||
Locale: "en",
|
||||
AllowCommands: true,
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
@@ -136,13 +150,14 @@ func parse(c *caddy.Controller) ([]*config, error) {
|
||||
Regexp: &Regexp{Raw: "\\/\\..+"},
|
||||
}},
|
||||
CSS: "",
|
||||
FileSystem: dir.Dir(baseScope),
|
||||
FileSystem: fileutils.Dir(baseScope),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fm.NoAuth = noAuth
|
||||
m := &config{FileManager: fm}
|
||||
m.SetBaseURL(baseURL)
|
||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
# hugo - a caddy plugin
|
||||
|
||||
[](https://caddy.community)
|
||||
|
||||
hugo fills the gap between Hugo and the browser. [Hugo][6] is an easy and fast static website generator. This plugin fills the gap between Hugo and the end-user, providing you a web interface to manage the whole website.
|
||||
|
||||
Using this plugin, you won't need to have your own computer to edit posts, neither regenerate your static website, because you can do all of that just through your browser. It is an implementation of [hacdias/filemanager][1] library.
|
||||
|
||||
**Requirements:** you need to have the hugo executable in your PATH. You can download it from its [official page][6].
|
||||
|
||||
## Get Started
|
||||
|
||||
To start using this plugin you just need to go to the [download Caddy page][3] and choose `http.hugo` in the directives section. For further information on how Caddy works refer to [its documentation][4].
|
||||
|
||||
The default credentials are `admin` for both the user and the password. It is highy recommended to change them after logging in for the first time and to use HTTPS. You can create more users and define their own permissions using the web interface.
|
||||
|
||||
## Syntax
|
||||
|
||||
```
|
||||
hugo [directory] [admin] {
|
||||
database path
|
||||
}
|
||||
```
|
||||
|
||||
+ `directory` is the path, relative or absolute to the directory of your Hugo files. Defaults to `./`.
|
||||
+ `admin` is the URL path where you will access the admin interface. Defaults to `/admin`.
|
||||
+ `path` is the database path where the settings will be stored. By default, the settings will be stored on [`.caddy`][5] folder.
|
||||
|
||||
## Database
|
||||
|
||||
By default the database will be stored on [`.caddy`][5] directory, in a sub-directory called `hugo`. Each file name is an hash of the combination of the host and the base URL.
|
||||
|
||||
If you don't set a database path, you will receive a warning like this:
|
||||
|
||||
> [WARNING] A database is going to be created for your File Manager instace at ~/.caddy/hugo/xxx.db. It is highly recommended that you set the 'database' option to 'xxx.db'
|
||||
|
||||
Why? If you don't set a database path and you change the host or the base URL, your settings will be reseted. So it is *highly* recommended to set this option.
|
||||
|
||||
When you set a relative path, such as `xxxxxxxxxx.db`, it will always be relative to `.caddy/hugo` directory. Although, you may also use an absolute path if you wish to store the database in other place.
|
||||
|
||||
## Examples
|
||||
|
||||
Manage the current working directory's Hugo website at `/admin` and display the ```public``` folder to the user.
|
||||
|
||||
```
|
||||
root public
|
||||
hugo {
|
||||
database myinstance.db
|
||||
}
|
||||
```
|
||||
|
||||
Manage the Hugo website located at `/var/www/mysite` at `/admin` and display the ```public``` folder to the user.
|
||||
|
||||
```
|
||||
root /var/www/mysite/public
|
||||
hugo /var/www/mysite {
|
||||
database myinstance.db
|
||||
}
|
||||
```
|
||||
|
||||
Manage the Hugo website located at `/var/www/mysite` at `/private` and display the ```public``` folder to the user.
|
||||
|
||||
```
|
||||
root /var/www/mysite/public
|
||||
hugo /var/www/mysite /private {
|
||||
database myinstance.db
|
||||
}
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
If you are having troubles **handling large files** you might need to check out the [`timeouts`][2] plugin, which can be used to change the default HTTP Timeouts.
|
||||
|
||||
[1]:https://github.com/hacdias/filemanager
|
||||
[2]:https://caddyserver.com/docs/timeouts
|
||||
[3]:https://caddyserver.com/download
|
||||
[4]:https://caddyserver.com/docs
|
||||
[5]:https://caddyserver.com/docs/automatic-https#dot-caddy
|
||||
[6]:http://gohugo.io
|
||||
@@ -1,194 +1,177 @@
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"log"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
rice "github.com/GeertJohan/go.rice"
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/filemanager/variables"
|
||||
"github.com/robfig/cron"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
type hugo struct {
|
||||
// Website root
|
||||
Root string `description:"The relative or absolute path to the place where your website is located."`
|
||||
// Public folder
|
||||
Public string `description:"The relative or absolute path to the public folder."`
|
||||
// Hugo executable path
|
||||
Exe string `description:"The absolute path to the Hugo executable or the command to execute."`
|
||||
// Hugo arguments
|
||||
Args []string `description:"The arguments to run when running Hugo"`
|
||||
// Indicates if we should clean public before a new publish.
|
||||
CleanPublic bool `description:"Indicates if the public folder should be cleaned before publishing the website."`
|
||||
// setup configures a new FileManager middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := parse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: admin interface to cgange options
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return plugin{Configs: configs, Next: next}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h hugo) BeforeAPI(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// 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 == "/settings" {
|
||||
var frontmatter string
|
||||
var err error
|
||||
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
||||
var (
|
||||
configs []*filemanager.FileManager
|
||||
)
|
||||
|
||||
if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil {
|
||||
frontmatter = "yaml"
|
||||
for c.Next() {
|
||||
// hugo [directory] [admin] {
|
||||
// database path
|
||||
// }
|
||||
directory := "."
|
||||
admin := "/admin"
|
||||
database := ""
|
||||
noAuth := false
|
||||
|
||||
// Get the baseURL and baseScope
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) >= 1 {
|
||||
directory = args[0]
|
||||
}
|
||||
|
||||
if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil {
|
||||
frontmatter = "json"
|
||||
if len(args) > 1 {
|
||||
admin = args[1]
|
||||
}
|
||||
|
||||
if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil {
|
||||
frontmatter = "toml"
|
||||
}
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "database":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
r.URL.Path = "/config." + frontmatter
|
||||
return 0, nil
|
||||
}
|
||||
database = c.Val()
|
||||
case "no_auth":
|
||||
if !c.NextArg() {
|
||||
noAuth = true
|
||||
continue
|
||||
}
|
||||
|
||||
// From here on, we only care about 'hugo' router so we can bypass
|
||||
// the others.
|
||||
if c.Router != "hugo" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
// If we are creating a file built from an archetype.
|
||||
if r.Header.Get("Archetype") != "" {
|
||||
if !c.User.AllowNew {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
filename = strings.TrimPrefix(filename, "/")
|
||||
archetype := r.Header.Get("archetype")
|
||||
|
||||
ext := filepath.Ext(filename)
|
||||
|
||||
// If the request isn't for a markdown file, we can't
|
||||
// handle it.
|
||||
if ext != ".markdown" && ext != ".md" {
|
||||
return http.StatusBadRequest, errUnsupportedFileType
|
||||
}
|
||||
|
||||
// Tries to create a new file based on this archetype.
|
||||
args := []string{"new", filename, "--kind", archetype}
|
||||
if err := Run(h.Exe, args, h.Root); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Writes the location of the new file to the Header.
|
||||
w.Header().Set("Location", "/files/content/"+filename)
|
||||
return http.StatusCreated, nil
|
||||
}
|
||||
|
||||
// If we are trying to regenerate the website.
|
||||
if r.Header.Get("Regenerate") == "true" {
|
||||
if !c.User.Permissions["allowPublish"] {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
filename = strings.TrimPrefix(filename, "/")
|
||||
|
||||
// Before save command handler.
|
||||
if err := c.FM.Runner("before_publish", filename); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// We only run undraft command if it is a file.
|
||||
if !strings.HasSuffix(filename, "/") {
|
||||
args := []string{"undraft", filename}
|
||||
if err := Run(h.Exe, args, h.Root); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
var err error
|
||||
noAuth, err = strconv.ParseBool(c.Val())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regenerates the file
|
||||
h.run(false)
|
||||
caddyConf := httpserver.GetConfig(c)
|
||||
|
||||
// Executed the before publish command.
|
||||
if err := c.FM.Runner("before_publish", filename); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
path := filepath.Join(caddy.AssetsPath(), "hugo")
|
||||
err := os.MkdirAll(path, 0700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
if r.Header.Get("Schedule") != "" {
|
||||
if !c.User.Permissions["allowPublish"] {
|
||||
return http.StatusForbidden, nil
|
||||
// if there is a database path and it is not absolute,
|
||||
// it will be relative to ".caddy" folder.
|
||||
if !filepath.IsAbs(database) && database != "" {
|
||||
database = filepath.Join(path, database)
|
||||
}
|
||||
|
||||
return h.schedule(c, w, r)
|
||||
// If there is no database path on the settings,
|
||||
// store one in .caddy/hugo/{name}.db.
|
||||
if database == "" {
|
||||
// The name of the database is the hashed value of a string composed
|
||||
// by the host, address path and the baseurl of this File Manager
|
||||
// instance.
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + admin))
|
||||
sha := hex.EncodeToString(hasher.Sum(nil))
|
||||
database = filepath.Join(path, sha+".db")
|
||||
|
||||
fmt.Println("[WARNING] A database is going to be created for your Hugo instace at " + database +
|
||||
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
||||
}
|
||||
|
||||
m, err := filemanager.New(database, filemanager.User{
|
||||
Locale: "en",
|
||||
AllowCommands: true,
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
AllowPublish: true,
|
||||
Commands: []string{"git", "svn", "hg"},
|
||||
Rules: []*filemanager.Rule{{
|
||||
Regex: true,
|
||||
Allow: false,
|
||||
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
||||
}},
|
||||
CSS: "",
|
||||
FileSystem: fileutils.Dir(directory),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize the default settings for Hugo.
|
||||
hugo := &filemanager.Hugo{
|
||||
Root: directory,
|
||||
Public: filepath.Join(directory, "public"),
|
||||
Args: []string{},
|
||||
CleanPublic: true,
|
||||
}
|
||||
|
||||
// Attaches Hugo plugin to this file manager instance.
|
||||
err = m.EnableStaticGen(hugo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.NoAuth = noAuth
|
||||
m.SetBaseURL(admin)
|
||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
||||
configs = append(configs, m)
|
||||
}
|
||||
|
||||
return http.StatusNotFound, nil
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (h hugo) AfterAPI(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (h hugo) JavaScript() string {
|
||||
return rice.MustFindBox("./").MustString("hugo.js")
|
||||
}
|
||||
|
||||
// run runs Hugo with the define arguments.
|
||||
func (h hugo) run(force bool) {
|
||||
// If the CleanPublic option is enabled, clean it.
|
||||
if h.CleanPublic {
|
||||
os.RemoveAll(h.Public)
|
||||
}
|
||||
|
||||
// Prevent running if watching is enabled
|
||||
if b, pos := variables.StringInSlice("--watch", h.Args); b && !force {
|
||||
if len(h.Args) > pos && h.Args[pos+1] != "false" {
|
||||
return
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
func (p plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for i := range p.Configs {
|
||||
// Checks if this Path should be handled by File Manager.
|
||||
if !httpserver.Path(r.URL.Path).Matches(p.Configs[i].BaseURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(h.Args) == pos+1 {
|
||||
return
|
||||
}
|
||||
p.Configs[i].ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if err := Run(h.Exe, h.Args, h.Root); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return p.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// schedule schedules a post to be published later.
|
||||
func (h hugo) schedule(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t, err := time.Parse("2006-01-02T15:04", r.Header.Get("Schedule"))
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
path = filepath.Clean(path)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
scheduler := cron.New()
|
||||
scheduler.AddFunc(t.Format("05 04 15 02 01 *"), func() {
|
||||
args := []string{"undraft", path}
|
||||
if err := Run(h.Exe, args, h.Root); err != nil {
|
||||
log.Printf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.run(false)
|
||||
func init() {
|
||||
caddy.RegisterPlugin("hugo", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
|
||||
scheduler.Start()
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
Next httpserver.Handler
|
||||
Configs []*filemanager.FileManager
|
||||
}
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
(function () {
|
||||
if (window.plugins === undefined || window.plugins === null) {
|
||||
window.plugins = []
|
||||
}
|
||||
|
||||
let regenerate = function (data, url) {
|
||||
url = data.api.removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${data.store.state.baseURL}/api/hugo${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`)
|
||||
request.setRequestHeader('Regenerate', 'true')
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
let newArchetype = function (data, url, type) {
|
||||
url = data.api.removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${data.store.state.baseURL}/api/hugo${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`)
|
||||
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.getResponseHeader('Location'))
|
||||
} else {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
let schedule = function (data, file, date) {
|
||||
file = data.api.removePrefix(file)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${data.store.state.baseURL}/api/hugo${file}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`)
|
||||
request.setRequestHeader('Schedule', date)
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.getResponseHeader('Location'))
|
||||
} else {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
window.plugins.push({
|
||||
name: 'hugo',
|
||||
credits: 'With a flavour of <a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-hugo">Hugo</a>.',
|
||||
header: {
|
||||
visible: [
|
||||
{
|
||||
if: function (data, route) {
|
||||
return (data.store.state.req.kind === 'editor' &&
|
||||
!data.store.state.loading &&
|
||||
data.store.state.req.metadata !== undefined &&
|
||||
data.store.state.req.metadata !== null &&
|
||||
data.store.state.user.allowEdit &
|
||||
data.store.state.user.permissions.allowPublish)
|
||||
},
|
||||
click: function (event, data, route) {
|
||||
event.preventDefault()
|
||||
document.getElementById('save-button').click()
|
||||
// TODO: wait for save to finish?
|
||||
data.buttons.loading('publish')
|
||||
|
||||
regenerate(data, route.path)
|
||||
.then(() => {
|
||||
data.buttons.done('publish')
|
||||
data.store.commit('showSuccess', 'Post published!')
|
||||
data.store.commit('setReload', true)
|
||||
})
|
||||
.catch((error) => {
|
||||
data.buttons.done('publish')
|
||||
data.store.commit('showError', error)
|
||||
})
|
||||
},
|
||||
id: 'publish-button',
|
||||
icon: 'send',
|
||||
name: 'Publish'
|
||||
}
|
||||
],
|
||||
hidden: [
|
||||
{
|
||||
if: function (data, route) {
|
||||
return (data.store.state.req.kind === 'editor' &&
|
||||
!data.store.state.loading &&
|
||||
data.store.state.req.metadata !== undefined &&
|
||||
data.store.state.req.metadata !== null &&
|
||||
data.store.state.user.permissions.allowPublish)
|
||||
},
|
||||
click: function (event, data, route) {
|
||||
document.getElementById('save-button').click()
|
||||
data.store.commit('showHover', 'schedule')
|
||||
},
|
||||
id: 'schedule-button',
|
||||
icon: 'alarm',
|
||||
name: 'Schedule'
|
||||
}
|
||||
]
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
click: function (event, data, route) {
|
||||
data.router.push({ path: '/files/settings' })
|
||||
},
|
||||
icon: 'settings',
|
||||
name: 'Hugo Settings'
|
||||
},
|
||||
{
|
||||
click: function (event, data, route) {
|
||||
data.store.commit('showHover', 'new-archetype')
|
||||
},
|
||||
if: function (data, route) {
|
||||
return data.store.state.user.allowNew
|
||||
},
|
||||
icon: 'merge_type',
|
||||
name: 'Hugo New'
|
||||
} /* ,
|
||||
{
|
||||
click: function (event, data, route) {
|
||||
console.log('evt')
|
||||
},
|
||||
icon: 'remove_red_eye',
|
||||
name: 'Preview'
|
||||
} */
|
||||
],
|
||||
prompts: [
|
||||
{
|
||||
name: 'new-archetype',
|
||||
title: 'New file',
|
||||
description: 'Create a new post based on an archetype. Your file will be created on content folder.',
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'file',
|
||||
placeholder: 'File name'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'archetype',
|
||||
placeholder: 'Archetype'
|
||||
}
|
||||
],
|
||||
ok: 'Create',
|
||||
submit: function (event, data, route) {
|
||||
event.preventDefault()
|
||||
|
||||
let file = event.currentTarget.querySelector('[name="file"]').value
|
||||
let type = event.currentTarget.querySelector('[name="archetype"]').value
|
||||
if (type === '') type = 'default'
|
||||
|
||||
data.store.commit('closeHovers')
|
||||
|
||||
newArchetype(data, '/' + file, type)
|
||||
.then((url) => {
|
||||
data.router.push({ path: url })
|
||||
})
|
||||
.catch(error => {
|
||||
data.store.commit('showError', error)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'schedule',
|
||||
title: 'Schedule',
|
||||
description: 'Pick a date and time to schedule the publication of this post.',
|
||||
inputs: [
|
||||
{
|
||||
type: 'datetime-local',
|
||||
name: 'date',
|
||||
placeholder: 'Date'
|
||||
}
|
||||
],
|
||||
ok: 'Schedule',
|
||||
submit: function (event, data, route) {
|
||||
event.preventDefault()
|
||||
data.buttons.loading('schedule')
|
||||
|
||||
let date = event.currentTarget.querySelector('[name="date"]').value
|
||||
if (date === '') {
|
||||
data.buttons.done('schedule')
|
||||
data.store.commit('showError', 'The date must not be empty.')
|
||||
return
|
||||
}
|
||||
|
||||
schedule(data, route.path, date)
|
||||
.then(() => {
|
||||
data.buttons.done('schedule')
|
||||
data.store.commit('showSuccess', 'Post scheduled!')
|
||||
})
|
||||
.catch((error) => {
|
||||
data.buttons.done('schedule')
|
||||
data.store.commit('showError', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
})()
|
||||
File diff suppressed because one or more lines are too long
@@ -1,19 +0,0 @@
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Run executes an external command
|
||||
func Run(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
|
||||
}
|
||||
@@ -1,27 +1,21 @@
|
||||
package hugo
|
||||
package jekyll
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/filemanager/dir"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
var (
|
||||
errHugoNotFound = errors.New("It seems that tou don't have 'hugo' on your PATH")
|
||||
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
|
||||
)
|
||||
|
||||
// setup configures a new FileManager middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := parse(c)
|
||||
@@ -42,17 +36,18 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
||||
)
|
||||
|
||||
for c.Next() {
|
||||
// hugo [directory] [admin] {
|
||||
// jekyll [directory] [admin] {
|
||||
// database path
|
||||
// }
|
||||
directory := "."
|
||||
admin := "/admin"
|
||||
database := ""
|
||||
noAuth := false
|
||||
|
||||
// Get the baseURL and baseScope
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) == 1 {
|
||||
if len(args) >= 1 {
|
||||
directory = args[0]
|
||||
}
|
||||
|
||||
@@ -68,12 +63,23 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
||||
}
|
||||
|
||||
database = c.Val()
|
||||
case "no_auth":
|
||||
if !c.NextArg() {
|
||||
noAuth = true
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
noAuth, err = strconv.ParseBool(c.Val())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
caddyConf := httpserver.GetConfig(c)
|
||||
|
||||
path := filepath.Join(caddy.AssetsPath(), "hugo")
|
||||
path := filepath.Join(caddy.AssetsPath(), "jekyll")
|
||||
err := os.MkdirAll(path, 0700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -86,7 +92,7 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
||||
}
|
||||
|
||||
// If there is no database path on the settings,
|
||||
// store one in .caddy/hugo/{name}.db.
|
||||
// store one in .caddy/jekyll/{name}.db.
|
||||
if database == "" {
|
||||
// The name of the database is the hashed value of a string composed
|
||||
// by the host, address path and the baseurl of this File Manager
|
||||
@@ -96,15 +102,16 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
||||
sha := hex.EncodeToString(hasher.Sum(nil))
|
||||
database = filepath.Join(path, sha+".db")
|
||||
|
||||
fmt.Println("[WARNING] A database is going to be created for your Hugo instace at " + database +
|
||||
fmt.Println("[WARNING] A database is going to be created for your Jekyll instace at " + database +
|
||||
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
||||
}
|
||||
|
||||
m, err := filemanager.New(database, filemanager.User{
|
||||
Locale: "en",
|
||||
AllowCommands: true,
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
Permissions: map[string]bool{},
|
||||
AllowPublish: true,
|
||||
Commands: []string{"git", "svn", "hg"},
|
||||
Rules: []*filemanager.Rule{{
|
||||
Regex: true,
|
||||
@@ -112,46 +119,28 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
||||
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
||||
}},
|
||||
CSS: "",
|
||||
FileSystem: dir.Dir(directory),
|
||||
FileSystem: fileutils.Dir(directory),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize the default settings for Hugo.
|
||||
hugo := &hugo{
|
||||
// Initialize the default settings for Jekyll.
|
||||
jekyll := &filemanager.Jekyll{
|
||||
Root: directory,
|
||||
Public: filepath.Join(directory, "public"),
|
||||
Public: filepath.Join(directory, "_site"),
|
||||
Args: []string{},
|
||||
CleanPublic: true,
|
||||
}
|
||||
|
||||
// Try to find the Hugo executable path.
|
||||
if hugo.Exe, err = exec.LookPath("hugo"); err != nil {
|
||||
return nil, errHugoNotFound
|
||||
}
|
||||
|
||||
err = m.RegisterPlugin("hugo", hugo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.RegisterEventType("before_publish")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.RegisterEventType("after_publish")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.RegisterPermission("allowPublish", true)
|
||||
// Attaches Hugo plugin to this file manager instance.
|
||||
err = m.EnableStaticGen(jekyll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.NoAuth = noAuth
|
||||
m.SetBaseURL(admin)
|
||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
||||
configs = append(configs, m)
|
||||
@@ -176,7 +165,7 @@ func (p plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
}
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("hugo", caddy.Plugin{
|
||||
caddy.RegisterPlugin("jekyll", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
@@ -1,31 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/filemanager/dir"
|
||||
)
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
// confFile contains the configuration file for this File Manager instance.
|
||||
// If the user chooses to use a configuration file, the flags will be ignored.
|
||||
type confFile struct {
|
||||
Database string `json:"database"`
|
||||
Scope string `json:"scope"`
|
||||
Address string `json:"address"`
|
||||
Commands []string `json:"commands"`
|
||||
Port int `json:"port"`
|
||||
AllowCommands bool `json:"allowCommands"`
|
||||
AllowEdit bool `json:"allowEdit"`
|
||||
AllowNew bool `json:"allowNew"`
|
||||
}
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/fileutils"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
addr string
|
||||
@@ -33,77 +24,188 @@ var (
|
||||
database string
|
||||
scope string
|
||||
commands string
|
||||
port string
|
||||
logfile string
|
||||
staticgen string
|
||||
locale string
|
||||
port int
|
||||
noAuth bool
|
||||
allowCommands bool
|
||||
allowEdit bool
|
||||
allowNew bool
|
||||
allowPublish bool
|
||||
showVer bool
|
||||
version = "master"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&config, "config", "", "JSON configuration file")
|
||||
flag.StringVar(&port, "port", "0", "HTTP Port (default is random)")
|
||||
flag.StringVar(&addr, "address", "", "Address to listen to (default is all of them)")
|
||||
flag.StringVar(&database, "database", "./filemanager.db", "Database path")
|
||||
flag.StringVar(&scope, "scope", ".", "Default scope for new users")
|
||||
flag.StringVar(&commands, "commands", "git svn hg", "Space separated commands available for new users")
|
||||
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option")
|
||||
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option")
|
||||
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option")
|
||||
flag.StringVarP(&config, "config", "c", "", "Configuration file")
|
||||
flag.IntVarP(&port, "port", "p", 0, "HTTP Port (default is random)")
|
||||
flag.StringVarP(&addr, "address", "a", "", "Address to listen to (default is all of them)")
|
||||
flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file")
|
||||
flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file")
|
||||
flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users")
|
||||
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
|
||||
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users")
|
||||
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
|
||||
flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users")
|
||||
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users")
|
||||
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
|
||||
flag.StringVar(&locale, "locale", "en", "Default locale for new users")
|
||||
flag.StringVar(&staticgen, "staticgen", "", "Static Generator you want to enable")
|
||||
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
|
||||
}
|
||||
|
||||
func setupViper() {
|
||||
viper.SetDefault("Address", "")
|
||||
viper.SetDefault("Port", "0")
|
||||
viper.SetDefault("Database", "./filemanager.db")
|
||||
viper.SetDefault("Scope", ".")
|
||||
viper.SetDefault("Logger", "stdout")
|
||||
viper.SetDefault("Commands", []string{"git", "svn", "hg"})
|
||||
viper.SetDefault("AllowCommmands", true)
|
||||
viper.SetDefault("AllowEdit", true)
|
||||
viper.SetDefault("AllowNew", true)
|
||||
viper.SetDefault("AllowPublish", true)
|
||||
viper.SetDefault("StaticGen", "")
|
||||
viper.SetDefault("Locale", "en")
|
||||
viper.SetDefault("NoAuth", false)
|
||||
|
||||
viper.BindPFlag("Port", flag.Lookup("port"))
|
||||
viper.BindPFlag("Address", flag.Lookup("address"))
|
||||
viper.BindPFlag("Database", flag.Lookup("database"))
|
||||
viper.BindPFlag("Scope", flag.Lookup("scope"))
|
||||
viper.BindPFlag("Logger", flag.Lookup("log"))
|
||||
viper.BindPFlag("Commands", flag.Lookup("commands"))
|
||||
viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
|
||||
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
|
||||
viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
|
||||
viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish"))
|
||||
viper.BindPFlag("Locale", flag.Lookup("locale"))
|
||||
viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
|
||||
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
|
||||
|
||||
viper.SetConfigName("filemanager")
|
||||
viper.AddConfigPath(".")
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
version = strings.TrimSpace(version)
|
||||
|
||||
if version == "" {
|
||||
fmt.Println("filemanager is at an untracked version")
|
||||
} else {
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
fmt.Println("filemanager version", version)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupViper()
|
||||
flag.Parse()
|
||||
|
||||
if config != "" {
|
||||
loadConfig()
|
||||
if showVer {
|
||||
printVersion()
|
||||
}
|
||||
|
||||
fm, err := filemanager.New(database, filemanager.User{
|
||||
AllowCommands: allowCommands,
|
||||
AllowEdit: allowEdit,
|
||||
AllowNew: allowNew,
|
||||
Commands: strings.Split(strings.TrimSpace(commands), " "),
|
||||
// Add a configuration file if set.
|
||||
if config != "" {
|
||||
ext := filepath.Ext(config)
|
||||
dir := filepath.Dir(config)
|
||||
config = strings.TrimSuffix(config, ext)
|
||||
|
||||
if dir != "" {
|
||||
viper.AddConfigPath(dir)
|
||||
config = strings.TrimPrefix(config, dir)
|
||||
}
|
||||
|
||||
viper.SetConfigName(config)
|
||||
}
|
||||
|
||||
// Read configuration from a file if exists.
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
if _, ok := err.(viper.ConfigParseError); ok {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up process log before anything bad happens.
|
||||
switch viper.GetString("Logger") {
|
||||
case "stdout":
|
||||
log.SetOutput(os.Stdout)
|
||||
case "stderr":
|
||||
log.SetOutput(os.Stderr)
|
||||
case "":
|
||||
log.SetOutput(ioutil.Discard)
|
||||
default:
|
||||
log.SetOutput(&lumberjack.Logger{
|
||||
Filename: logfile,
|
||||
MaxSize: 100,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 10,
|
||||
})
|
||||
}
|
||||
|
||||
// Create a File Manager instance.
|
||||
fm, err := filemanager.New(viper.GetString("Database"), filemanager.User{
|
||||
AllowCommands: viper.GetBool("AllowCommands"),
|
||||
AllowEdit: viper.GetBool("AllowEdit"),
|
||||
AllowNew: viper.GetBool("AllowNew"),
|
||||
AllowPublish: viper.GetBool("AllowPublish"),
|
||||
Commands: viper.GetStringSlice("Commands"),
|
||||
Rules: []*filemanager.Rule{},
|
||||
Locale: viper.GetString("Locale"),
|
||||
CSS: "",
|
||||
FileSystem: dir.Dir(scope),
|
||||
FileSystem: fileutils.Dir(viper.GetString("Scope")),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if viper.GetBool("NoAuth") {
|
||||
fm.NoAuth = true
|
||||
}
|
||||
|
||||
fm.SetBaseURL("/")
|
||||
fm.SetPrefixURL("/")
|
||||
|
||||
listener, err := net.Listen("tcp", addr+":"+port)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
switch viper.GetString("StaticGen") {
|
||||
case "hugo":
|
||||
hugo := &filemanager.Hugo{
|
||||
Root: viper.GetString("Scope"),
|
||||
Public: filepath.Join(viper.GetString("Scope"), "public"),
|
||||
Args: []string{},
|
||||
CleanPublic: true,
|
||||
}
|
||||
|
||||
if err = fm.EnableStaticGen(hugo); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "jekyll":
|
||||
jekyll := &filemanager.Jekyll{
|
||||
Root: viper.GetString("Scope"),
|
||||
Public: filepath.Join(viper.GetString("Scope"), "_site"),
|
||||
Args: []string{"build"},
|
||||
CleanPublic: true,
|
||||
}
|
||||
|
||||
if err = fm.EnableStaticGen(jekyll); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Builds the address and a listener.
|
||||
laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
|
||||
listener, err := net.Listen("tcp", laddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Tell the user the port in which is listening.
|
||||
fmt.Println("Listening on", listener.Addr().String())
|
||||
|
||||
// Starts the server.
|
||||
if err := http.Serve(listener, fm); err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() {
|
||||
file, err := ioutil.ReadFile(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var conf *confFile
|
||||
err = json.Unmarshal(file, &conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
database = conf.Database
|
||||
scope = conf.Scope
|
||||
addr = conf.Address
|
||||
commands = strings.Join(conf.Commands, " ")
|
||||
port = strconv.Itoa(conf.Port)
|
||||
allowNew = conf.AllowNew
|
||||
allowEdit = conf.AllowEdit
|
||||
allowCommands = conf.AllowCommands
|
||||
}
|
||||
|
||||
230
dir/dir.go
230
dir/dir.go
@@ -1,230 +0,0 @@
|
||||
// Package dir implements a FileSystem interface using the native
|
||||
// file system restricted to a specific directory tree. Originally from
|
||||
// https://github.com/golang/net/blob/master/webdav/file.go#L68
|
||||
package dir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Dir uses the native file system restricted to a specific directory tree.
|
||||
//
|
||||
// While the FileSystem.OpenFile method takes '/'-separated paths, a Dir's
|
||||
// string value is a filename on the native file system, not a URL, so it is
|
||||
// separated by filepath.Separator, which isn't necessarily '/'.
|
||||
//
|
||||
// An empty Dir is treated as ".".
|
||||
type Dir string
|
||||
|
||||
func (d Dir) resolve(name string) string {
|
||||
// This implementation is based on Dir.Open's code in the standard net/http package.
|
||||
if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
|
||||
strings.Contains(name, "\x00") {
|
||||
return ""
|
||||
}
|
||||
|
||||
dir := string(d)
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
|
||||
return filepath.Join(dir, filepath.FromSlash(SlashClean(name)))
|
||||
}
|
||||
|
||||
// Mkdir implements os.Mkdir in this directory context.
|
||||
func (d Dir) Mkdir(name string, perm os.FileMode) error {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
return os.Mkdir(name, perm)
|
||||
}
|
||||
|
||||
// OpenFile implements os.OpenFile in this directory context.
|
||||
func (d Dir) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
f, err := os.OpenFile(name, flag, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// RemoveAll implements os.RemoveAll in this directory context.
|
||||
func (d Dir) RemoveAll(name string) error {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
if name == filepath.Clean(string(d)) {
|
||||
// Prohibit removing the virtual root directory.
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return os.RemoveAll(name)
|
||||
}
|
||||
|
||||
// Rename implements os.Rename in this directory context.
|
||||
func (d Dir) Rename(oldName, newName string) error {
|
||||
if oldName = d.resolve(oldName); oldName == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if newName = d.resolve(newName); newName == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if root := filepath.Clean(string(d)); root == oldName || root == newName {
|
||||
// Prohibit renaming from or to the virtual root directory.
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return os.Rename(oldName, newName)
|
||||
}
|
||||
|
||||
// Stat implements os.Stat in this directory context.
|
||||
func (d Dir) Stat(name string) (os.FileInfo, error) {
|
||||
if name = d.resolve(name); name == "" {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
return os.Stat(name)
|
||||
}
|
||||
|
||||
// Copy copies a file or directory from src to dst. If it is
|
||||
// a directory, all of the files and sub-directories will be copied.
|
||||
func (d Dir) Copy(src, dst string) error {
|
||||
if src = d.resolve(src); src == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
if dst = d.resolve(dst); dst == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
if root := filepath.Clean(string(d)); root == src || root == dst {
|
||||
// Prohibit copying from or to the virtual root directory.
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
if dst == src {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
info, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return CopyDir(src, dst)
|
||||
}
|
||||
|
||||
return CopyFile(src, dst)
|
||||
}
|
||||
|
||||
// SlashClean is equivalent to but slightly more efficient than
|
||||
// path.Clean("/" + name).
|
||||
func SlashClean(name string) string {
|
||||
if name == "" || name[0] != '/' {
|
||||
name = "/" + name
|
||||
}
|
||||
return path.Clean(name)
|
||||
}
|
||||
|
||||
// CopyFile copies a file from source to dest and returns
|
||||
// an error if any.
|
||||
func CopyFile(source string, dest string) error {
|
||||
// Open the source file.
|
||||
src, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Makes the directory needed to create the dst
|
||||
// file.
|
||||
err = os.MkdirAll(filepath.Dir(dest), 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the destination file.
|
||||
dst, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// Copy the contents of the file.
|
||||
_, err = io.Copy(dst, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy the mode if the user can't
|
||||
// open the file.
|
||||
info, err := os.Stat(source)
|
||||
if err != nil {
|
||||
err = os.Chmod(dest, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyDir copies a directory from source to dest and all
|
||||
// of its sub-directories. It doesn't stop if it finds an error
|
||||
// during the copy. Returns an error if any.
|
||||
func CopyDir(source string, dest string) error {
|
||||
// Get properties of source.
|
||||
srcinfo, err := os.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the destination directory.
|
||||
err = os.MkdirAll(dest, srcinfo.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir, _ := os.Open(source)
|
||||
obs, err := dir.Readdir(-1)
|
||||
|
||||
var errs []error
|
||||
|
||||
for _, obj := range obs {
|
||||
fsource := source + "/" + obj.Name()
|
||||
fdest := dest + "/" + obj.Name()
|
||||
|
||||
if obj.IsDir() {
|
||||
// Create sub-directories, recursively.
|
||||
err = CopyDir(fsource, fdest)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
} else {
|
||||
// Perform the file copy.
|
||||
err = CopyFile(fsource, fdest)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errString string
|
||||
for _, err := range errs {
|
||||
errString += err.Error() + "\n"
|
||||
}
|
||||
|
||||
if errString != "" {
|
||||
return errors.New(errString)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
56
doc.go
56
doc.go
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
Package filemanager provides a web interface to access your files
|
||||
wherever you are. To use this package as a middleware for your app,
|
||||
you'll need to create a filemanager instance:
|
||||
|
||||
m, err := filemanager.New(database, user)
|
||||
|
||||
Where 'user' contains the default options for new users. You can just
|
||||
use 'filemanager.DefaultUser' or create yourself a default user:
|
||||
|
||||
m, err := filemanager.New(database, filemanager.User{
|
||||
Admin: false,
|
||||
AllowCommands: false,
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
Commands: []string{
|
||||
"git",
|
||||
},
|
||||
Rules: []*filemanager.Rule{},
|
||||
CSS: "",
|
||||
FileSystem: webdav.Dir("/path/to/files"),
|
||||
})
|
||||
|
||||
The credentials for the first user are always 'admin' for both the user and
|
||||
the password, and they can be changed later through the settings. The first
|
||||
user is always an Admin and has all of the permissions set to 'true'.
|
||||
|
||||
Then, you should set the Prefix URL and the Base URL, using the following
|
||||
functions:
|
||||
|
||||
m.SetBaseURL("/")
|
||||
m.SetPrefixURL("/")
|
||||
|
||||
The Prefix URL is a part of the path that is already stripped from the
|
||||
r.URL.Path variable before the request arrives to File Manager's handler.
|
||||
This is a function that will rarely be used. You can see one example on Caddy
|
||||
filemanager plugin.
|
||||
|
||||
The Base URL is the URL path where you want File Manager to be available in. If
|
||||
you want to be available at the root path, you should call:
|
||||
|
||||
m.SetBaseURL("/")
|
||||
|
||||
But if you want to access it at '/admin', you would call:
|
||||
|
||||
m.SetBaseURL("/admin")
|
||||
|
||||
Now, that you already have a File Manager instance created, you just need to
|
||||
add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
|
||||
We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
|
||||
|
||||
One simple implementation for this, at port 80, in the root of the domain, would be:
|
||||
|
||||
http.ListenAndServe(":80", m)
|
||||
*/
|
||||
package filemanager
|
||||
18
download.go
18
download.go
@@ -9,7 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hacdias/filemanager/dir"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/archiver"
|
||||
)
|
||||
|
||||
@@ -20,14 +20,14 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// If the file isn't a directory, serve it using http.ServeFile. We display it
|
||||
// inline if it is requested.
|
||||
if !c.FI.IsDir {
|
||||
if !c.File.IsDir {
|
||||
if r.URL.Query().Get("inline") == "true" {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.FI.Name)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name)
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, c.FI.Path)
|
||||
http.ServeFile(w, r, c.File.Path)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -45,15 +45,15 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Clean the slashes.
|
||||
name = dir.SlashClean(name)
|
||||
files = append(files, filepath.Join(c.FI.Path, name))
|
||||
name = fileutils.SlashClean(name)
|
||||
files = append(files, filepath.Join(c.File.Path, name))
|
||||
}
|
||||
} else {
|
||||
files = append(files, c.FI.Path)
|
||||
files = append(files, c.File.Path)
|
||||
}
|
||||
|
||||
// If the format is true, just set it to "zip".
|
||||
if query == "true" {
|
||||
if query == "true" || query == "" {
|
||||
query = "zip"
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Defines the file name.
|
||||
name := c.FI.Name
|
||||
name := c.File.Name
|
||||
if name == "." || name == "" {
|
||||
name = "download"
|
||||
}
|
||||
|
||||
23
file.go
23
file.go
@@ -20,7 +20,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -110,7 +110,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
|
||||
func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||
// Gets the directory information using the Virtual File System of
|
||||
// the user configuration.
|
||||
f, err := c.User.FileSystem.OpenFile(c.FI.VirtualPath, os.O_RDONLY, 0)
|
||||
f, err := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -328,6 +328,8 @@ func (l listing) ApplySort() {
|
||||
sort.Sort(sort.Reverse(byName(l)))
|
||||
case "size":
|
||||
sort.Sort(sort.Reverse(bySize(l)))
|
||||
case "modified":
|
||||
sort.Sort(sort.Reverse(byModified(l)))
|
||||
default:
|
||||
// If not one of the above, do nothing
|
||||
return
|
||||
@@ -338,6 +340,8 @@ func (l listing) ApplySort() {
|
||||
sort.Sort(byName(l))
|
||||
case "size":
|
||||
sort.Sort(bySize(l))
|
||||
case "modified":
|
||||
sort.Sort(byModified(l))
|
||||
default:
|
||||
sort.Sort(byName(l))
|
||||
return
|
||||
@@ -348,6 +352,7 @@ func (l listing) ApplySort() {
|
||||
// Implement sorting for listing
|
||||
type byName listing
|
||||
type bySize listing
|
||||
type byModified listing
|
||||
|
||||
// By Name
|
||||
func (l byName) Len() int {
|
||||
@@ -392,6 +397,20 @@ func (l bySize) Less(i, j int) bool {
|
||||
return iSize < jSize
|
||||
}
|
||||
|
||||
// By Modified
|
||||
func (l byModified) Len() int {
|
||||
return len(l.Items)
|
||||
}
|
||||
|
||||
func (l byModified) Swap(i, j int) {
|
||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
||||
}
|
||||
|
||||
func (l byModified) Less(i, j int) bool {
|
||||
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
|
||||
return iModified.Sub(jModified) < 0
|
||||
}
|
||||
|
||||
var textExtensions = [...]string{
|
||||
".md", ".markdown", ".mdown", ".mmark",
|
||||
".asciidoc", ".adoc", ".ad",
|
||||
|
||||
283
filemanager.go
283
filemanager.go
@@ -1,3 +1,56 @@
|
||||
// Package filemanager provides a web interface to access your files
|
||||
// wherever you are. To use this package as a middleware for your app,
|
||||
// you'll need to create a filemanager instance:
|
||||
//
|
||||
// m, err := filemanager.New(database, user)
|
||||
//
|
||||
// Where 'user' contains the default options for new users. You can just
|
||||
// use 'filemanager.DefaultUser' or create yourself a default user:
|
||||
//
|
||||
// m, err := filemanager.New(database, filemanager.User{
|
||||
// Admin: false,
|
||||
// AllowCommands: false,
|
||||
// AllowEdit: true,
|
||||
// AllowNew: true,
|
||||
// Commands: []string{
|
||||
// "git",
|
||||
// },
|
||||
// Rules: []*filemanager.Rule{},
|
||||
// CSS: "",
|
||||
// FileSystem: webdav.Dir("/path/to/files"),
|
||||
// })
|
||||
//
|
||||
// The credentials for the first user are always 'admin' for both the user and
|
||||
// the password, and they can be changed later through the settings. The first
|
||||
// user is always an Admin and has all of the permissions set to 'true'.
|
||||
//
|
||||
// Then, you should set the Prefix URL and the Base URL, using the following
|
||||
// functions:
|
||||
//
|
||||
// m.SetBaseURL("/")
|
||||
// m.SetPrefixURL("/")
|
||||
//
|
||||
// The Prefix URL is a part of the path that is already stripped from the
|
||||
// r.URL.Path variable before the request arrives to File Manager's handler.
|
||||
// This is a function that will rarely be used. You can see one example on Caddy
|
||||
// filemanager plugin.
|
||||
//
|
||||
// The Base URL is the URL path where you want File Manager to be available in. If
|
||||
// you want to be available at the root path, you should call:
|
||||
//
|
||||
// m.SetBaseURL("/")
|
||||
//
|
||||
// But if you want to access it at '/admin', you would call:
|
||||
//
|
||||
// m.SetBaseURL("/admin")
|
||||
//
|
||||
// Now, that you already have a File Manager instance created, you just need to
|
||||
// add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
|
||||
// We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
|
||||
//
|
||||
// One simple implementation for this, at port 80, in the root of the domain, would be:
|
||||
//
|
||||
// http.ListenAndServe(":80", m)
|
||||
package filemanager
|
||||
|
||||
import (
|
||||
@@ -6,20 +59,27 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
rice "github.com/GeertJohan/go.rice"
|
||||
"github.com/asdine/storm"
|
||||
"github.com/hacdias/filemanager/dir"
|
||||
"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")
|
||||
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
|
||||
@@ -34,6 +94,9 @@ type FileManager struct {
|
||||
// The static assets.
|
||||
assets *rice.Box
|
||||
|
||||
// Job cron.
|
||||
cron *cron.Cron
|
||||
|
||||
// PrefixURL is a part of the URL that is already trimmed from the request URL before it
|
||||
// arrives to our handlers. It may be useful when using File Manager as a middleware
|
||||
// such as in caddy-filemanager plugin. It is only useful in certain situations.
|
||||
@@ -44,6 +107,15 @@ type FileManager struct {
|
||||
// edited directly. Use SetBaseURL.
|
||||
BaseURL string
|
||||
|
||||
// NoAuth disables the authentication. When the authentication is disabled,
|
||||
// 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
|
||||
|
||||
// The Default User needed to build the New User page.
|
||||
DefaultUser *User
|
||||
|
||||
@@ -52,9 +124,6 @@ type FileManager struct {
|
||||
|
||||
// A map of events to a slice of commands.
|
||||
Commands map[string][]string
|
||||
|
||||
// The plugins that have been plugged in.
|
||||
Plugins map[string]Plugin
|
||||
}
|
||||
|
||||
// Command is a command function.
|
||||
@@ -76,7 +145,7 @@ type User struct {
|
||||
Admin bool `json:"admin"`
|
||||
|
||||
// FileSystem is the virtual file system the user has access.
|
||||
FileSystem dir.Dir `json:"filesystem"`
|
||||
FileSystem fileutils.Dir `json:"filesystem"`
|
||||
|
||||
// Rules is an array of access and deny rules.
|
||||
Rules []*Rule `json:"rules"`
|
||||
@@ -84,11 +153,14 @@ type User struct {
|
||||
// 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
|
||||
Permissions map[string]bool `json:"permissions"` // Permissions added by plugins
|
||||
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"`
|
||||
@@ -115,28 +187,18 @@ type Regexp struct {
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// Plugin is a File Manager plugin.
|
||||
type Plugin interface {
|
||||
// The JavaScript that will be injected into the main page.
|
||||
JavaScript() string
|
||||
|
||||
// If the Plugin returns (0, nil), the executation of File Manager will procced as usual.
|
||||
// Otherwise it will stop.
|
||||
BeforeAPI(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
||||
AfterAPI(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// DefaultUser is used on New, when no 'base' user is provided.
|
||||
var DefaultUser = User{
|
||||
AllowCommands: true,
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
Permissions: map[string]bool{},
|
||||
AllowPublish: true,
|
||||
Commands: []string{},
|
||||
Rules: []*Rule{},
|
||||
CSS: "",
|
||||
Admin: true,
|
||||
FileSystem: dir.Dir("."),
|
||||
Locale: "en",
|
||||
FileSystem: fileutils.Dir("."),
|
||||
}
|
||||
|
||||
// New creates a new File Manager instance. If 'database' file already
|
||||
@@ -147,9 +209,9 @@ func New(database string, base User) (*FileManager, error) {
|
||||
// Creates a new File Manager instance with the Users
|
||||
// map and Assets box.
|
||||
m := &FileManager{
|
||||
Users: map[string]*User{},
|
||||
assets: rice.MustFindBox("./assets/dist"),
|
||||
Plugins: map[string]Plugin{},
|
||||
Users: map[string]*User{},
|
||||
cron: cron.New(),
|
||||
assets: rice.MustFindBox("./assets/dist"),
|
||||
}
|
||||
|
||||
// Tries to open a database on the location provided. This
|
||||
@@ -183,8 +245,10 @@ func New(database string, base User) (*FileManager, error) {
|
||||
err = db.Get("config", "commands", &m.Commands)
|
||||
if err != nil && err == storm.ErrNotFound {
|
||||
m.Commands = map[string][]string{
|
||||
"before_save": {},
|
||||
"after_save": {},
|
||||
"before_save": {},
|
||||
"after_save": {},
|
||||
"before_publish": {},
|
||||
"after_publish": {},
|
||||
}
|
||||
err = db.Set("config", "commands", m.Commands)
|
||||
}
|
||||
@@ -222,6 +286,7 @@ func New(database string, base User) (*FileManager, error) {
|
||||
u.AllowCommands = true
|
||||
u.AllowNew = true
|
||||
u.AllowEdit = true
|
||||
u.AllowPublish = true
|
||||
|
||||
// Saves the user to the database.
|
||||
if err := db.Save(&u); err != nil {
|
||||
@@ -238,6 +303,10 @@ func New(database string, base User) (*FileManager, error) {
|
||||
base.Username = ""
|
||||
base.Password = ""
|
||||
m.DefaultUser = &base
|
||||
|
||||
m.cron.AddFunc("@hourly", m.shareCleaner)
|
||||
m.cron.Start()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -265,87 +334,107 @@ func (m *FileManager) SetBaseURL(url string) {
|
||||
m.BaseURL = strings.TrimSuffix(url, "/")
|
||||
}
|
||||
|
||||
// RegisterPlugin registers a plugin to a File Manager instance and
|
||||
// loads its options from the database.
|
||||
func (m *FileManager) RegisterPlugin(name string, plugin Plugin) error {
|
||||
if _, ok := m.Plugins[name]; ok {
|
||||
return errors.New("Plugin already registred")
|
||||
}
|
||||
// ServeHTTP handles the request.
|
||||
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
code, err := serveHTTP(&RequestContext{
|
||||
FileManager: m,
|
||||
User: nil,
|
||||
File: nil,
|
||||
}, w, r)
|
||||
|
||||
err := m.db.Get("plugins", name, &plugin)
|
||||
if err != nil && err == storm.ErrNotFound {
|
||||
err = m.db.Set("plugins", name, plugin)
|
||||
if code >= 400 {
|
||||
w.WriteHeader(code)
|
||||
|
||||
if err == nil {
|
||||
txt := http.StatusText(code)
|
||||
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
|
||||
w.Write([]byte(txt))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Plugins[name] = plugin
|
||||
return nil
|
||||
}
|
||||
m.staticgen = "hugo"
|
||||
m.StaticGen = h
|
||||
|
||||
// RegisterEventType registers a new event type which can be triggered using Runner
|
||||
// function.
|
||||
func (m *FileManager) RegisterEventType(name string) error {
|
||||
if _, ok := m.Commands[name]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.Commands[name] = []string{}
|
||||
return m.db.Set("config", "commands", m.Commands)
|
||||
}
|
||||
|
||||
// RegisterPermission registers a new user permission and adds it to every
|
||||
// user with it default's 'value'. If the user is an admin, it will
|
||||
// be true.
|
||||
func (m *FileManager) RegisterPermission(name string, value bool) error {
|
||||
if _, ok := m.DefaultUser.Permissions[name]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add the default value for this permission on the default user.
|
||||
m.DefaultUser.Permissions[name] = value
|
||||
|
||||
for _, u := range m.Users {
|
||||
// Bypass the user if it is already defined.
|
||||
if _, ok := u.Permissions[name]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if u.Permissions == nil {
|
||||
u.Permissions = m.DefaultUser.Permissions
|
||||
}
|
||||
|
||||
if u.Admin {
|
||||
u.Permissions[name] = true
|
||||
}
|
||||
|
||||
err := m.db.Save(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := m.db.Get("staticgen", "hugo", h)
|
||||
if err != nil && err == storm.ErrNotFound {
|
||||
err = m.db.Set("staticgen", "hugo", *h)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
// Compatible with http.Handler.
|
||||
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
code, err := serveHTTP(&RequestContext{
|
||||
FM: m,
|
||||
User: nil,
|
||||
FI: nil,
|
||||
}, w, r)
|
||||
func (m *FileManager) enableJekyll(j *Jekyll) error {
|
||||
if err := j.find(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if code != 0 {
|
||||
w.WriteHeader(code)
|
||||
if len(j.Args) == 0 {
|
||||
j.Args = []string{"build"}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.Write([]byte(err.Error()))
|
||||
} else {
|
||||
w.Write([]byte(http.StatusText(code)))
|
||||
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.
|
||||
func (m FileManager) shareCleaner() {
|
||||
var links []shareLink
|
||||
|
||||
// Get all links.
|
||||
err := m.db.All(&links)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find the expired ones.
|
||||
for i := range links {
|
||||
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
|
||||
err = m.db.DeleteStruct(&links[i])
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hacdias/filemanager/dir"
|
||||
"github.com/hacdias/fileutils"
|
||||
)
|
||||
|
||||
type test struct {
|
||||
@@ -28,13 +28,13 @@ func newTest(t *testing.T) *test {
|
||||
scope := filepath.Join(temp, "scope")
|
||||
database := filepath.Join(temp, "database.db")
|
||||
|
||||
err = dir.CopyDir("./testdata", scope)
|
||||
err = fileutils.CopyDir("./testdata", scope)
|
||||
if err != nil {
|
||||
t.Fatalf("Error copying the test data: %v", err)
|
||||
}
|
||||
|
||||
user := DefaultUser
|
||||
user.FileSystem = dir.Dir(scope)
|
||||
user.FileSystem = fileutils.Dir(scope)
|
||||
|
||||
fm, err := New(database, user)
|
||||
|
||||
|
||||
148
http.go
148
http.go
@@ -6,13 +6,16 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
)
|
||||
|
||||
// RequestContext contains the needed information to make handlers work.
|
||||
type RequestContext struct {
|
||||
*FileManager
|
||||
User *User
|
||||
FM *FileManager
|
||||
FI *file
|
||||
File *file
|
||||
// On API handlers, Router is the APi handler we want.
|
||||
Router string
|
||||
}
|
||||
@@ -21,9 +24,9 @@ type RequestContext struct {
|
||||
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.FM.BaseURL)
|
||||
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
|
||||
|
||||
if len(p) >= len(r.URL.Path) && c.FM.BaseURL != "" {
|
||||
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
@@ -33,10 +36,9 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
// pass it through a template to add the needed variables.
|
||||
if r.URL.Path == "/sw.js" {
|
||||
return renderFile(
|
||||
w,
|
||||
c.FM.assets.MustString("sw.js"),
|
||||
c, w,
|
||||
c.assets.MustString("sw.js"),
|
||||
"application/javascript",
|
||||
c,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,31 +60,41 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
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/") && c.StaticGen != nil {
|
||||
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(
|
||||
w,
|
||||
c.FM.assets.MustString("index.html"),
|
||||
c, w,
|
||||
c.assets.MustString("index.html"),
|
||||
"text/html",
|
||||
c,
|
||||
)
|
||||
}
|
||||
|
||||
// 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.FM.assets.HTTPBox()).ServeHTTP(w, r)
|
||||
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return renderFile(
|
||||
w,
|
||||
c.FM.assets.MustString("static/manifest.json"),
|
||||
c, w,
|
||||
c.assets.MustString("static/manifest.json"),
|
||||
"application/json",
|
||||
c,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -107,8 +119,15 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
for _, p := range c.FM.Plugins {
|
||||
code, err := p.BeforeAPI(c, w, r)
|
||||
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
|
||||
}
|
||||
@@ -116,7 +135,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
|
||||
if c.Router == "checksum" || c.Router == "download" {
|
||||
var err error
|
||||
c.FI, err = getInfo(r.URL, c.FM, c.User)
|
||||
c.File, err = getInfo(r.URL, c.FileManager, c.User)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
}
|
||||
@@ -138,25 +157,14 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
code, err = resourceHandler(c, w, r)
|
||||
case "users":
|
||||
code, err = usersHandler(c, w, r)
|
||||
case "commands":
|
||||
code, err = commandsHandler(c, w, r)
|
||||
case "plugins":
|
||||
code, err = pluginsHandler(c, w, r)
|
||||
case "settings":
|
||||
code, err = settingsHandler(c, w, r)
|
||||
case "share":
|
||||
code, err = shareHandler(c, w, r)
|
||||
default:
|
||||
code = http.StatusNotFound
|
||||
}
|
||||
|
||||
if code >= 300 || err != nil {
|
||||
return code, err
|
||||
}
|
||||
|
||||
for _, p := range c.FM.Plugins {
|
||||
code, err := p.AfterAPI(c, w, r)
|
||||
if code != 0 || err != nil {
|
||||
return code, err
|
||||
}
|
||||
}
|
||||
|
||||
return code, err
|
||||
}
|
||||
|
||||
@@ -164,7 +172,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
query := r.URL.Query().Get("algo")
|
||||
|
||||
val, err := c.FI.Checksum(query)
|
||||
val, err := c.File.Checksum(query)
|
||||
if err == errInvalidOption {
|
||||
return http.StatusBadRequest, err
|
||||
} else if err != nil {
|
||||
@@ -193,19 +201,13 @@ func splitURL(path string) (string, string) {
|
||||
}
|
||||
|
||||
// renderFile renders a file using a template with some needed variables.
|
||||
func renderFile(w http.ResponseWriter, file string, contentType string, c *RequestContext) (int, error) {
|
||||
functions := template.FuncMap{
|
||||
"JS": func(s string) template.JS {
|
||||
return template.JS(s)
|
||||
},
|
||||
}
|
||||
|
||||
tpl := template.Must(template.New("file").Funcs(functions).Parse(file))
|
||||
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.FM.RootURL(),
|
||||
"Plugins": c.FM.Plugins,
|
||||
"BaseURL": c.RootURL(),
|
||||
"StaticGen": c.staticgen,
|
||||
})
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
@@ -214,6 +216,66 @@ func renderFile(w http.ResponseWriter, file string, contentType string, c *Reque
|
||||
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)
|
||||
|
||||
35
package.json
35
package.json
@@ -9,58 +9,63 @@
|
||||
"lint": "eslint --ext .js,.vue assets/src"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^1.7.1",
|
||||
"codemirror": "^5.27.4",
|
||||
"filesize": "^3.5.10",
|
||||
"moment": "^2.18.1",
|
||||
"normalize.css": "^7.0.0",
|
||||
"vue": "^2.3.3",
|
||||
"vue-i18n": "^7.1.0",
|
||||
"vue-router": "^2.7.0",
|
||||
"vuex": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.7.2",
|
||||
"autoprefixer": "^7.1.2",
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^6.2.10",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-preset-env": "^1.3.2",
|
||||
"babel-preset-stage-2": "^6.22.0",
|
||||
"babel-register": "^6.22.0",
|
||||
"chalk": "^1.1.3",
|
||||
"chalk": "^2.0.1",
|
||||
"connect-history-api-fallback": "^1.3.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-standard": "^6.2.1",
|
||||
"eslint-friendly-formatter": "^2.0.7",
|
||||
"eslint": "^4.3.0",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-friendly-formatter": "^3.0.0",
|
||||
"eslint-loader": "^1.7.1",
|
||||
"eslint-plugin-html": "^2.0.0",
|
||||
"eslint-plugin-html": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-node": "^5.1.1",
|
||||
"eslint-plugin-promise": "^3.4.0",
|
||||
"eslint-plugin-standard": "^2.0.1",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.14.1",
|
||||
"extract-text-webpack-plugin": "^2.0.0",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^0.11.1",
|
||||
"friendly-errors-webpack-plugin": "^1.1.3",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"http-proxy-middleware": "^0.17.3",
|
||||
"opn": "^4.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "^1.3.0",
|
||||
"opn": "^5.1.0",
|
||||
"optimize-css-assets-webpack-plugin": "^3.0.0",
|
||||
"ora": "^1.2.0",
|
||||
"rimraf": "^2.6.0",
|
||||
"semver": "^5.3.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"sw-precache-webpack-plugin": "^0.9.1",
|
||||
"sw-precache-webpack-plugin": "^0.11.4",
|
||||
"uglify-js": "^3.0.23",
|
||||
"url-loader": "^0.5.8",
|
||||
"vue-loader": "^12.1.0",
|
||||
"vue-loader": "^13.0.2",
|
||||
"vue-style-loader": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.3",
|
||||
"webpack": "^2.6.1",
|
||||
"webpack": "^3.4.1",
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
"webpack-dev-middleware": "^1.10.0",
|
||||
"webpack-hot-middleware": "^2.18.0",
|
||||
"webpack-merge": "^4.1.0"
|
||||
"webpack-merge": "^4.1.0",
|
||||
"yml-loader": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
|
||||
94
resource.go
94
resource.go
@@ -4,19 +4,22 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hacdias/filemanager/dir"
|
||||
"github.com/hacdias/fileutils"
|
||||
)
|
||||
|
||||
// sanitizeURL sanitizes the URL to prevent path transversal
|
||||
// using dir.SlashClean and adds the trailing slash bar.
|
||||
// using fileutils.SlashClean and adds the trailing slash bar.
|
||||
func sanitizeURL(url string) string {
|
||||
path := dir.SlashClean(url)
|
||||
path := fileutils.SlashClean(url)
|
||||
if strings.HasSuffix(url, "/") && path != "/" {
|
||||
return path + "/"
|
||||
}
|
||||
@@ -34,7 +37,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
case http.MethodPut:
|
||||
// Before save command handler.
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
if err := c.FM.Runner("before_save", path); err != nil {
|
||||
if err := c.Runner("before_save", path); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -44,7 +47,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// After save command handler.
|
||||
if err := c.FM.Runner("after_save", path); err != nil {
|
||||
if err := c.Runner("after_save", path); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -60,7 +63,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Gets the information of the directory/file.
|
||||
f, err := getInfo(r.URL, c.FM, c.User)
|
||||
f, err := getInfo(r.URL, c.FileManager, c.User)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
}
|
||||
@@ -73,7 +76,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// If it is a dir, go and serve the listing.
|
||||
if f.IsDir {
|
||||
c.FI = f
|
||||
c.File = f
|
||||
return listingHandler(c, w, r)
|
||||
}
|
||||
|
||||
@@ -101,7 +104,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
f := c.FI
|
||||
f := c.File
|
||||
f.Kind = "listing"
|
||||
|
||||
// Tries to get the listing data.
|
||||
@@ -112,7 +115,7 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
|
||||
listing := f.listing
|
||||
|
||||
// Defines the cookie scope.
|
||||
cookieScope := c.FM.RootURL()
|
||||
cookieScope := c.RootURL()
|
||||
if cookieScope == "" {
|
||||
cookieScope = "/"
|
||||
}
|
||||
@@ -155,6 +158,12 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Discard any invalid upload before returning to avoid connection
|
||||
// reset error.
|
||||
defer func() {
|
||||
io.Copy(ioutil.Discard, r.Body)
|
||||
}()
|
||||
|
||||
// Checks if the current request is for a directory and not a file.
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// If the method is PUT, we return 405 Method not Allowed, because
|
||||
@@ -164,21 +173,21 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
// Otherwise we try to create the directory.
|
||||
err := c.User.FileSystem.Mkdir(r.URL.Path, 0666)
|
||||
err := c.User.FileSystem.Mkdir(r.URL.Path, 0776)
|
||||
return errorToHTTP(err, false), err
|
||||
}
|
||||
|
||||
// If using POST method, we are trying to create a new file so it is not
|
||||
// desirable to override an already existent file. Thus, we check
|
||||
// if the file already exists. If so, we just return a 409 Conflict.
|
||||
if r.Method == http.MethodPost {
|
||||
if r.Method == http.MethodPost && r.Header.Get("Action") != "override" {
|
||||
if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil {
|
||||
return http.StatusConflict, errors.New("There is already a file on that path")
|
||||
}
|
||||
}
|
||||
|
||||
// Create/Open the file.
|
||||
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
}
|
||||
@@ -196,12 +205,73 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
|
||||
return errorToHTTP(err, false), err
|
||||
}
|
||||
|
||||
// Check if this instance has a Static Generator and handles publishing
|
||||
// or scheduling if it's the case.
|
||||
if c.StaticGen != nil {
|
||||
code, err := resourcePublishSchedule(c, w, r)
|
||||
if code != 0 {
|
||||
return code, err
|
||||
}
|
||||
}
|
||||
|
||||
// Writes the ETag Header.
|
||||
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
|
||||
w.Header().Set("ETag", etag)
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
publish := r.Header.Get("Publish")
|
||||
schedule := r.Header.Get("Schedule")
|
||||
|
||||
if publish != "true" && schedule == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if !c.User.AllowPublish {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
if publish == "true" {
|
||||
return resourcePublish(c, w, r)
|
||||
}
|
||||
|
||||
t, err := time.Parse("2006-01-02T15:04", schedule)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
c.cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
|
||||
_, err := resourcePublish(c, w, r)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
})
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
|
||||
// Before save command handler.
|
||||
if err := c.Runner("before_publish", path); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
code, err := c.StaticGen.Publish(c, w, r)
|
||||
if err != nil {
|
||||
return code, err
|
||||
}
|
||||
|
||||
// Executed the before publish command.
|
||||
if err := c.Runner("before_publish", path); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// resourcePatchHandler is the entry point for resource handler.
|
||||
func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.AllowEdit {
|
||||
|
||||
@@ -1 +1 @@
|
||||
be97c942365e7a765698a8c0175b4eec18cb4899
|
||||
f3386142a6c9f000e2481b7456e10f94c5097beb
|
||||
137
settings.go
137
settings.go
@@ -1,104 +1,131 @@
|
||||
package filemanager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
func commandsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
return commandsGetHandler(c, w, r)
|
||||
case http.MethodPut:
|
||||
return commandsPutHandler(c, w, r)
|
||||
}
|
||||
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
type modifySettingsRequest struct {
|
||||
*modifyRequest
|
||||
Data struct {
|
||||
Commands map[string][]string `json:"commands"`
|
||||
StaticGen map[string]interface{} `json:"staticGen"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func commandsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
return renderJSON(w, c.FM.Commands)
|
||||
type option struct {
|
||||
Variable string `json:"variable"`
|
||||
Name string `json:"name"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
func commandsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
|
||||
// Checks if the request body is empty.
|
||||
if r.Body == nil {
|
||||
return http.StatusBadGateway, errors.New("Empty request body")
|
||||
return nil, errEmptyRequest
|
||||
}
|
||||
|
||||
var commands map[string][]string
|
||||
|
||||
// Parses the user and checks for error.
|
||||
err := json.NewDecoder(r.Body).Decode(&commands)
|
||||
// Parses the request body and checks if it's well formed.
|
||||
mod := &modifySettingsRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(mod)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, errors.New("Invalid JSON")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.FM.db.Set("config", "commands", commands); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
// Checks if the request type is right.
|
||||
if mod.What != "settings" {
|
||||
return nil, errWrongDataType
|
||||
}
|
||||
|
||||
c.FM.Commands = commands
|
||||
return http.StatusOK, nil
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
func pluginsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if r.URL.Path != "" && r.URL.Path != "/" {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
return pluginsGetHandler(c, w, r)
|
||||
return settingsGetHandler(c, w, r)
|
||||
case http.MethodPut:
|
||||
return pluginsPutHandler(c, w, r)
|
||||
return settingsPutHandler(c, w, r)
|
||||
}
|
||||
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
func pluginsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
return renderJSON(w, c.FM.Plugins)
|
||||
type settingsGetRequest struct {
|
||||
Commands map[string][]string `json:"commands"`
|
||||
StaticGen []option `json:"staticGen"`
|
||||
}
|
||||
|
||||
func pluginsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
if r.Body == nil {
|
||||
return http.StatusBadGateway, errors.New("Empty request body")
|
||||
result := &settingsGetRequest{
|
||||
Commands: c.Commands,
|
||||
StaticGen: []option{},
|
||||
}
|
||||
|
||||
var raw map[string]map[string]interface{}
|
||||
if c.StaticGen != nil {
|
||||
t := reflect.TypeOf(c.StaticGen).Elem()
|
||||
|
||||
// Parses the user and checks for error.
|
||||
err := json.NewDecoder(r.Body).Decode(&raw)
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if t.Field(i).Name[0] == bytes.ToLower([]byte{t.Field(i).Name[0]})[0] {
|
||||
continue
|
||||
}
|
||||
|
||||
result.StaticGen = append(result.StaticGen, option{
|
||||
Variable: t.Field(i).Name,
|
||||
Name: t.Field(i).Tag.Get("name"),
|
||||
Value: reflect.ValueOf(c.StaticGen).Elem().FieldByName(t.Field(i).Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return renderJSON(w, result)
|
||||
}
|
||||
|
||||
func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
mod, err := parsePutSettingsRequest(r)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
for name, plugin := range raw {
|
||||
err = mapstructure.Decode(plugin, c.FM.Plugins[name])
|
||||
if err != nil {
|
||||
// Update the commands.
|
||||
if mod.Which == "commands" {
|
||||
if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
err = c.FM.db.Set("plugins", name, c.FM.Plugins[name])
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
c.Commands = mod.Data.Commands
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
// Update the static generator options.
|
||||
if mod.Which == "staticGen" {
|
||||
err = mapstructure.Decode(mod.Data.StaticGen, c.StaticGen)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
err = c.db.Set("staticgen", c.staticgen, c.StaticGen)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user