StaticGen update Bash

Update zh-cn.yaml (#194)

Address #184

build assets

Update zh-cn.yaml (#194)


Former-commit-id: 4572f93371647c0a6b53cc375ec5bb00356a37c9 [formerly e58e4793ac0ca6915b605d7b2a8a77f0aec31172] [formerly 43635f6b98f546ec0e2656d26031388aef63a902 [formerly da4fd84002]]
Former-commit-id: 15422887ad29dea63bdf861d9da8f8d28b4fbc8f [formerly 3949ffa499cf999c3f4b50ee18802c0f87a23807]
Former-commit-id: fac5ceeee3fa969239d9ef5e04ef5542a61d2761
This commit is contained in:
Henrique Dias 2017-08-09 15:06:28 +01:00
parent bfdb924cb7
commit d5e943069e
28 changed files with 689 additions and 902 deletions

View File

@ -5,6 +5,7 @@
<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">
@ -26,8 +27,6 @@
if (file.match(/\.(js|css)$/)) { %>
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<!-- Plugins info -->
<script>{{ .JavaScript }}</script>
<style>
#loading {
position: fixed;

View File

@ -16,19 +16,11 @@
<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="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
@ -52,19 +44,9 @@
<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>
@ -91,6 +73,7 @@ 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 {mapGetters, mapState} from 'vuex'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
@ -106,7 +89,8 @@ export default {
CopyButton,
UploadButton,
SwitchButton,
MoveButton
MoveButton,
ScheduleButton
},
data: function () {
return {
@ -134,7 +118,7 @@ export default {
'loading',
'reload',
'multiple',
'plugins'
'staticGen'
]),
isMobile () {
return this.width <= 736
@ -148,6 +132,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
},

View File

@ -17,10 +17,32 @@
</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>
<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>
<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>
@ -38,7 +60,6 @@
<p class="credits">
<span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
<span v-for="plugin in plugins" :key="plugin.name" v-html="plugin.credits"><br></span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
</p>
</nav>
@ -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
}
}

View 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>

View File

@ -17,7 +17,7 @@ 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,22 +112,30 @@ 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.done(button)
this.$store.commit('setSchedule', '')
})
.catch(error => {
buttons.done('save')
buttons.done(button)
this.$store.commit('showError', error)
this.$store.commit('setSchedule', '')
})
}
}

View 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>

View File

@ -12,33 +12,8 @@
<error v-else-if="showError"></error>
<success v-else-if="showSuccess"></success>
<replace v-else-if="showReplace"></replace>
<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"
:aria-label="prompt.ok"
:title="prompt.ok"
:value="prompt.ok">
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</form>
</template>
<schedule v-else-if="show === 'schedule'"></schedule>
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</template>
@ -55,7 +30,9 @@ 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 { mapState } from 'vuex'
import buttons from '@/utils/buttons'
import api from '@/utils/api'
@ -65,6 +42,8 @@ export default {
components: {
Info,
Delete,
NewArchetype,
Schedule,
Rename,
Error,
Download,

View 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>

View File

@ -20,7 +20,9 @@ buttons:
save: Save
search: Search
select: Select
publish: Publish
selectMultiple: Select multiple
schedule: Schedule
switchView: Swicth view
toggleSidebar: Toggle sidebar
update: Update
@ -93,12 +95,16 @@ prompts:
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.
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
@ -122,7 +128,6 @@ settings:
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.
pluginsUpdated: Plugins settings updated!
profileSettings: Profile Settings
ruleExample1: >
'prevents the access to any dot file (such as .git, .gitignore) in
@ -158,6 +163,9 @@ sidebar:
newFolder: New folder
servedWith: Served with
settings: Settings
siteSettings: Site Settings
hugoNew: Hugo New
preview: Preview
search:
images: Images
music: Music

View File

@ -14,10 +14,12 @@ buttons:
next: Próximo
ok: OK
previous: Anterior
publish: Publicar
rename: Renomear
replace: Substituir
reportIssue: Reportar Erro
save: Guardar
schedule: Agendar
search: Pesquisar
select: Selecionar
selectMultiple: Selecionar múltiplos
@ -30,11 +32,11 @@ errors:
internal: Algo correu bastante mal.
notFound: Não conseguimos chegar a esta localização.
files:
folders: Pastas
files: Ficheiros
body: Corpo
clear: Limpar
closePreview: Fechar pré-visualização
files: Ficheiros
folders: Pastas
home: Início
lastModified: Última modificação
loading: A carregar...
@ -43,9 +45,9 @@ files:
multipleSelectionEnabled: Seleção múltipla ativada
name: Nome
size: Tamanho
sortByLastModified: Ordenar pela última modificação
sortByName: Ordenar pelo nome
sortBySize: Ordenar pelo tamanho
sortByLastModified: Ordenar pela última modificação
help:
click: selecionar pasta ou ficheiro
ctrl:
@ -58,6 +60,10 @@ help:
f1: esta informação
f2: renomear ficheiro
help: Ajuda
languages:
en: Inglês
pt: Português
zhCN: Chinês (Simplificado)
login:
password: Palavra-passe
submit: Login
@ -79,6 +85,8 @@ prompts:
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
@ -89,22 +97,38 @@ prompts:
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?
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,
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!
@ -119,32 +143,32 @@ settings:
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.
pluginsUpdated: Plugins atualizados!
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
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
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},
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:'
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
@ -153,26 +177,12 @@ settings:
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
search:
images: Imagens
music: Música
pdf: PDF
writeToSearch: Escreva aqui para pesquisar
searchOrCommand: Pesquise ou execute um comando...
searchOrSupportedCommand: 'Pesquise ou utilize um dos seus comandos:'
search: Pesquise...
type: Escreva e prima enter para pesquisar.
types: Tipos
video: Vídeos
pressToSearch: Prima enter para pesquisar.
pressToExecute: Prima enter para executar.
languages:
en: Inglês
pt: Português
zhCN: Chinês (Simplificado)
siteSettings: Configurações do Site

View File

@ -120,7 +120,6 @@ settings:
permissionsHelp: >
'您可以将该用户设置为管理员 或单独选择各项权限. 如果选择 "管理员(Administrator)" ,
将自动检查所有其他选项, 并且该用户可以管理其他用户.'
pluginsUpdated: 插件设置更新!
profileSettings: 配置文件设置
ruleExample1: >
'阻止用户访问每个文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore).'
@ -152,13 +151,18 @@ sidebar:
servedWith: 服务提供
settings: 设置
search:
writeToSearch: 请输入要搜索的内容
images: 图像
music: 音乐
pdf: PDF
pressToExecute: 按 Enter 键(回车)执行.
pressToSearch: 按 Enter 键(回车)进行搜索.
search: 搜索...
searchOrCommand: 搜索或者执行命令(Linux 代码)...
searchOrSupportedCommand: '搜索或使用您支持使用的命令(一次只能执行一个命令):'
search: 搜索...
type: 键入并按 Enter 键(回车)进行搜索.
pressToSearch: 按 Enter 键(回车)进行搜索.
pressToExecute: 按 Enter 键(回车)执行.
types: 类型
video: 视频
writeToSearch: 请输入要搜索的内容
languages:
en: English
pt: Portuguese

View File

@ -8,13 +8,14 @@ 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: [],

View File

@ -32,6 +32,9 @@ const mutations = {
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
@ -53,6 +56,9 @@ const mutations = {
resetClipboard: (state) => {
state.clipboard.key = ''
state.clipboard.items = []
},
setSchedule: (state, value) => {
state.schedule = value
}
}

View File

@ -85,13 +85,18 @@ export function post (url, content = '', overwrite = false) {
})
}
export 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) {

View File

@ -15,17 +15,15 @@
<h1>{{ $t('settings.globalSettings') }}</h1>
<form @submit="savePlugin" v-if="plugins.length > 0">
<template v-for="plugin in plugins">
<h2>{{ capitalize(plugin.name) }}</h2>
<form @submit="saveStaticGen" v-if="$store.state.staticGen.length > 0">
<h2>{{ capitalize($store.state.staticGen) }}</h2>
<p v-for="field in plugin.fields" :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>
</template>
<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>
@ -55,7 +53,7 @@ export default {
data: function () {
return {
commands: [],
plugins: []
staticGen: []
}
},
computed: {
@ -64,8 +62,8 @@ export default {
created () {
getSettings()
.then(settings => {
for (let key in settings.plugins) {
this.plugins.push(this.parsePlugin(key, settings.plugins[key]))
if (this.$store.state.staticGen.length > 0) {
this.parseStaticGen(settings.staticGen)
}
for (let key in settings.commands) {
@ -108,40 +106,29 @@ export default {
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
.catch(error => { this.showError(error) })
},
savePlugin (event) {
saveStaticGen (event) {
event.preventDefault()
let plugins = {}
let staticGen = {}
for (let plugin of this.plugins) {
let p = {}
for (let field of this.staticGen) {
staticGen[field.variable] = field.value
for (let field of plugin.fields) {
p[field.variable] = field.value
if (field.original === 'array') {
let val = field.value.split(' ')
if (val[0] === '') {
val.shift()
}
p[field.variable] = val
if (field.original === 'array') {
let val = field.value.split(' ')
if (val[0] === '') {
val.shift()
}
}
plugins[plugin.name] = p
staticGen[field.variable] = val
}
}
updateSettings(plugins, 'plugins')
.then(() => { this.showSuccess(this.$t('settings.pluginsUpdated')) })
updateSettings(staticGen, 'staticGen')
.then(() => { this.showSuccess(this.$t('settings.settingsUpdated')) })
.catch(error => { this.showError(error) })
},
parsePlugin (name, plugin) {
let obj = {
name: name,
fields: []
}
for (let option of plugin) {
parseStaticGen (staticgen) {
for (let option of staticgen) {
let value = option.value
let field = {
@ -156,7 +143,7 @@ export default {
field.original = 'array'
field.value = value.join(' ')
obj.fields.push(field)
this.staticGen.push(field)
continue
}
@ -167,10 +154,8 @@ export default {
break
}
obj.fields.push(field)
this.staticGen.push(field)
}
return obj
}
}
}

View File

@ -28,9 +28,7 @@
<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-for="(value, key) in permissions" :key="key">
<input type="checkbox" :disabled="admin" v-model="permissions[key]"> {{ capitalize(key) }}
</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>
@ -94,6 +92,7 @@ export default {
allowNew: false,
allowEdit: false,
allowCommands: false,
allowPublish: false,
permissions: {},
password: '',
username: '',
@ -120,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
}
@ -140,6 +140,7 @@ export default {
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(' ')
@ -183,6 +184,7 @@ export default {
this.admin = false
this.allowNew = false
this.allowEdit = false
this.allowPublish = false
this.permissins = {}
this.allowCommands = false
this.password = ''
@ -241,6 +243,7 @@ export default {
allowCommands: this.allowCommands,
allowNew: this.allowNew,
allowEdit: this.allowEdit,
allowPublish: this.allowPublish,
permissions: this.permissions,
css: this.css,
locale: this.locale,

View File

@ -11,7 +11,6 @@ import (
"strings"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/plugins"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
@ -112,7 +111,6 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
Permissions: map[string]bool{},
Commands: []string{"git", "svn", "hg"},
Rules: []*filemanager.Rule{{
Regex: true,
@ -128,20 +126,15 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
}
// Initialize the default settings for Hugo.
hugo := &plugins.Hugo{
hugo := &filemanager.Hugo{
Root: directory,
Public: filepath.Join(directory, "public"),
Args: []string{},
CleanPublic: true,
}
// Try to find the Hugo executable path.
if err = hugo.Find(); err != nil {
return nil, err
}
// Attaches Hugo plugin to this file manager instance.
err = m.ActivatePlugin("hugo", hugo)
err = m.EnableStaticGen(hugo)
if err != nil {
return nil, err
}

View File

@ -12,8 +12,6 @@ import (
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/hacdias/filemanager/plugins"
"github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
flag "github.com/spf13/pflag"
@ -27,7 +25,7 @@ var (
scope string
commands string
logfile string
plugin string
staticgen string
locale string
port int
noAuth bool
@ -51,7 +49,7 @@ func init() {
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(&plugin, "plugin", "", "Plugin you want to enable")
flag.StringVar(&staticgen, "staticgen", "", "Static Generator you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
}
@ -65,7 +63,7 @@ func setupViper() {
viper.SetDefault("AllowCommmands", true)
viper.SetDefault("AllowEdit", true)
viper.SetDefault("AllowNew", true)
viper.SetDefault("Plugin", "")
viper.SetDefault("StaticGen", "")
viper.SetDefault("Locale", "en")
viper.SetDefault("NoAuth", false)
@ -79,7 +77,7 @@ func setupViper() {
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
viper.BindPFlag("Locale", flag.Lookup("locale"))
viper.BindPFlag("Plugin", flag.Lookup("plugin"))
viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
viper.SetConfigName("filemanager")
@ -166,21 +164,16 @@ func main() {
log.Fatal(err)
}
if viper.GetString("Plugin") == "hugo" {
if viper.GetString("StaticGen") == "hugo" {
// Initialize the default settings for Hugo.
hugo := &plugins.Hugo{
hugo := &filemanager.Hugo{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{},
CleanPublic: true,
}
// Try to find the Hugo executable path.
if err = hugo.Find(); err != nil {
log.Fatal(err)
}
if err = fm.ActivatePlugin("hugo", hugo); err != nil {
if err = fm.EnableStaticGen(hugo); err != nil {
log.Fatal(err)
}
}

View File

@ -78,7 +78,6 @@ var (
errEmptyScope = errors.New("scope is empty")
errWrongDataType = errors.New("wrong data type")
errInvalidUpdateField = errors.New("invalid field to update")
plugins = map[string]Plugin{}
)
// FileManager is a file manager instance. It should be creating using the
@ -107,6 +106,11 @@ type FileManager struct {
// there will only exist one user, called "admin".
NoAuth bool
// staticgen is the name of the current static website generator.
staticgen string
// StaticGen is the static websit generator handler.
StaticGen StaticGen
// The Default User needed to build the New User page.
DefaultUser *User
@ -115,9 +119,15 @@ type FileManager struct {
// A map of events to a slice of commands.
Commands map[string][]string
}
// The options of the plugins that have been plugged into this instance.
Plugins map[string]interface{}
type StaticGen interface {
SettingsPath() string
Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Schedule(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
}
// Command is a command function.
@ -151,10 +161,10 @@ type User struct {
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"`
@ -175,43 +185,18 @@ type Rule struct {
Regexp *Regexp `json:"regexp"`
}
type PluginHandler func(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
type Plugin struct {
JavaScript string
CommandEvents []string
Permissions []Permission
Options interface{}
Handlers map[string]PluginHandler `json:"-"`
BeforeAPI PluginHandler `json:"-"`
AfterAPI PluginHandler `json:"-"`
}
type Permission struct {
Name string
Value bool
}
func RegisterPlugin(name string, plugin Plugin) {
if _, ok := plugins[name]; ok {
panic(name + " plugin is already registred")
}
plugins[name] = plugin
}
// 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: "",
@ -228,9 +213,8 @@ 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{},
Plugins: map[string]interface{}{},
assets: rice.MustFindBox("./assets/dist"),
Users: map[string]*User{},
assets: rice.MustFindBox("./assets/dist"),
}
// Tries to open a database on the location provided. This
@ -264,8 +248,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)
}
@ -346,95 +332,7 @@ func (m *FileManager) SetBaseURL(url string) {
m.BaseURL = strings.TrimSuffix(url, "/")
}
// ActivatePlugin activates a plugin to a File Manager instance and
// loads its options from the database.
func (m *FileManager) ActivatePlugin(name string, options interface{}) error {
if reflect.TypeOf(options).Kind() != reflect.Ptr {
return errors.New("options should be a pointer to interface, not interface")
}
var plugin Plugin
if p, ok := plugins[name]; !ok {
plugin = p
return errors.New(name + " plugin is not registred")
}
if _, ok := m.Plugins[name]; ok {
return errors.New(name + " plugin is already activated")
}
err := m.db.Get("plugins", name, &plugin)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("plugin", name, plugin)
}
if err != nil {
return err
}
// Register the command event hooks.
for _, evt := range plugin.CommandEvents {
if _, ok := m.Commands[evt]; ok {
continue
}
m.Commands[evt] = []string{}
}
err = m.db.Set("config", "commands", m.Commands)
if err != nil {
return err
}
// Register the user permissions.
for _, perm := range plugin.Permissions {
err = m.registerPermission(perm.Name, perm.Value)
if err != nil {
return err
}
}
m.Plugins[name] = options
return nil
}
// 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
}
}
return nil
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
// Compatible with http.Handler.
// ServeHTTP handles the request.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := serveHTTP(&RequestContext{
FileManager: m,
@ -458,6 +356,34 @@ func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
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)
}
return errors.New("unknown static website generator")
}
func (m *FileManager) enableHugo(hugo *Hugo) error {
if err := hugo.find(); err != nil {
return err
}
m.staticgen = "hugo"
m.StaticGen = hugo
err := m.db.Get("staticgen", "hugo", hugo)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "hugo", *hugo)
}
return nil
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule

62
http.go
View File

@ -58,28 +58,11 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return apiHandler(c, w, r)
}
// Checks if any plugin has an handler for this URL.
for p := range c.Plugins {
var h PluginHandler
for path, handler := range plugins[p].Handlers {
if strings.HasPrefix(r.URL.Path, path) {
h = handler
r.URL.Path = strings.TrimPrefix(r.URL.Path, path)
break
}
}
if h == nil {
continue
}
valid, _ := validateAuth(c, r)
if !valid {
return http.StatusForbidden, nil
}
return h(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)
}
// Any other request should show the index.html file.
@ -131,12 +114,15 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return http.StatusForbidden, nil
}
for p := range c.Plugins {
if plugins[p].BeforeAPI == nil {
continue
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()
}
code, err := plugins[p].BeforeAPI(c, w, r)
// Executes the Static website generator hook.
code, err := c.StaticGen.Hook(c, w, r)
if code != 0 || err != nil {
return code, err
}
@ -172,21 +158,6 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
code = http.StatusNotFound
}
if code >= 300 || err != nil {
return code, err
}
for p := range c.Plugins {
if plugins[p].AfterAPI == nil {
continue
}
code, err := plugins[p].AfterAPI(c, w, r)
if code != 0 || err != nil {
return code, err
}
}
return code, err
}
@ -227,14 +198,9 @@ func renderFile(w http.ResponseWriter, file string, contentType string, c *Reque
tpl := template.Must(template.New("file").Parse(file))
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
var javascript = ""
for name := range c.Plugins {
javascript += plugins[name].JavaScript + "\n"
}
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(),
"JavaScript": template.JS(javascript),
"BaseURL": c.RootURL(),
"StaticGen": c.staticgen,
})
if err != nil {
return http.StatusInternalServerError, err

View File

@ -1,258 +0,0 @@
package plugins
import (
"errors"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/hacdias/filemanager"
"github.com/hacdias/varutils"
"github.com/robfig/cron"
)
func init() {
filemanager.RegisterPlugin("hugo", filemanager.Plugin{
JavaScript: hugoJavaScript,
CommandEvents: []string{"before_publish", "after_publish"},
BeforeAPI: beforeAPI,
Handlers: map[string]filemanager.PluginHandler{
"/preview": previewHandler,
},
Permissions: []filemanager.Permission{
{
Name: "allowPublish",
Value: true,
},
},
})
}
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")
)
// Hugo is a hugo (https://gohugo.io) plugin.
type Hugo struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Hugo executable path
Exe string `name:"Hugo Executable"`
// Hugo arguments
Args []string `name:"Hugo Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// Find finds the hugo executable in the path.
func (h *Hugo) Find() error {
var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil {
return ErrHugoNotFound
}
return nil
}
// 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 := varutils.StringInSlice("--watch", h.Args); b && !force {
if len(h.Args) > pos && h.Args[pos+1] != "false" {
return
}
if len(h.Args) == pos+1 {
return
}
}
if err := Run(h.Exe, h.Args, h.Root); err != nil {
log.Println(err)
}
}
// 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() {
if err := h.undraft(path); err != nil {
log.Printf(err.Error())
return
}
h.run(false)
})
scheduler.Start()
return http.StatusOK, nil
}
func (h Hugo) undraft(file string) error {
args := []string{"undraft", file}
if err := Run(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") {
return err
}
return nil
}
func beforeAPI(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
o := c.Plugins["hugo"].(*Hugo)
// 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
if _, err = os.Stat(filepath.Join(o.Root, "config.yaml")); err == nil {
frontmatter = "yaml"
}
if _, err = os.Stat(filepath.Join(o.Root, "config.json")); err == nil {
frontmatter = "json"
}
if _, err = os.Stat(filepath.Join(o.Root, "config.toml")); err == nil {
frontmatter = "toml"
}
r.URL.Path = "/config." + frontmatter
}
// 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)
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(o.Exe, args, o.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)
// Before save command handler.
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
// We only run undraft command if it is a file.
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
if err := o.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
}
// Regenerates the file
o.run(false)
// Executed the before publish command.
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
if r.Header.Get("Schedule") != "" {
if !c.User.Permissions["allowPublish"] {
return http.StatusForbidden, nil
}
return o.schedule(c, w, r)
}
return http.StatusNotFound, nil
}
func previewHandler(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
h := c.Plugins["hugo"].(*Hugo)
// Get a new temporary path if there is none.
if h.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
h.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := h.Args
args = append(args, "--baseURL", c.RootURL()+"/preview/")
args = append(args, "--buildDrafts")
args = append(args, "--destination", h.previewPath)
// Builds the preview.
if err := Run(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r)
return 0, nil
}

View File

@ -1,227 +0,0 @@
package plugins
const hugoJavaScript = `'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.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) {
window.open(data.store.state.baseURL + '/preview/')
},
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)
})
}
}
]
})
})()`

View File

@ -1,19 +0,0 @@
package plugins
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
}

View File

@ -203,12 +203,40 @@ 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 c.StaticGen.Publish(c, w, r)
}
return c.StaticGen.Schedule(c, w, r)
}
// resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit {

View File

@ -1 +1 @@
824bbb2a05401392205f299b61d082c54ed3f3d2
fb7f000455844aaf5021aa2a43f0fcd05f67fa35

View File

@ -1,6 +1,7 @@
package filemanager
import (
"bytes"
"encoding/json"
"net/http"
"reflect"
@ -11,12 +12,12 @@ import (
type modifySettingsRequest struct {
*modifyRequest
Data struct {
Commands map[string][]string `json:"commands"`
Plugins map[string]map[string]interface{} `json:"plugins"`
Commands map[string][]string `json:"commands"`
StaticGen map[string]interface{} `json:"staticGen"`
} `json:"data"`
}
type pluginOption struct {
type option struct {
Variable string `json:"variable"`
Name string `json:"name"`
Value interface{} `json:"value"`
@ -59,8 +60,8 @@ func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
type settingsGetRequest struct {
Commands map[string][]string `json:"commands"`
Plugins map[string][]pluginOption `json:"plugins"`
Commands map[string][]string `json:"commands"`
StaticGen []option `json:"staticGen"`
}
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
@ -69,19 +70,22 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
result := &settingsGetRequest{
Commands: c.Commands,
Plugins: map[string][]pluginOption{},
Commands: c.Commands,
StaticGen: []option{},
}
for name, p := range c.Plugins {
result.Plugins[name] = []pluginOption{}
if c.StaticGen != nil {
t := reflect.TypeOf(c.StaticGen).Elem()
t := reflect.TypeOf(p).Elem()
for i := 0; i < t.NumField(); i++ {
result.Plugins[name] = append(result.Plugins[name], pluginOption{
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(p).Elem().FieldByName(t.Field(i).Name).Interface(),
Value: reflect.ValueOf(c.StaticGen).Elem().FieldByName(t.Field(i).Name).Interface(),
})
}
}
@ -108,18 +112,16 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusOK, nil
}
// Update the plugins.
if mod.Which == "plugins" {
for name, plugin := range mod.Data.Plugins {
err = mapstructure.Decode(plugin, c.Plugins[name])
if err != nil {
return http.StatusInternalServerError, err
}
// 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("plugins", name, c.Plugins[name])
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

239
staticgen.go Normal file
View File

@ -0,0 +1,239 @@
package filemanager
import (
"errors"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/hacdias/varutils"
"github.com/robfig/cron"
)
var (
// ErrHugoNotFound ...
ErrHugoNotFound = errors.New("It seems that tou don't have 'hugo' on your PATH")
// ErrUnsupportedFileType ...
ErrUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
)
// Hugo is the Hugo static website generator.
type Hugo struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Hugo executable path
Exe string `name:"Hugo Executable"`
// Hugo arguments
Args []string `name:"Hugo Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// SettingsPath retrieves the correct settings path.
func (h Hugo) SettingsPath() string {
var frontmatter string
var err error
if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil {
frontmatter = "yaml"
}
if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil {
frontmatter = "json"
}
if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil {
frontmatter = "toml"
}
if frontmatter == "" {
return "/settings"
}
return "/config." + frontmatter
}
// Hook is the pre-api handler.
func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method.
if r.Method != http.MethodPost {
return 0, nil
}
if c.Router != "resource" {
return 0, nil
}
// We only care about creating new files from archetypes here. So...
if r.Header.Get("Archetype") == "" {
return 0, nil
}
if !c.User.AllowNew {
return http.StatusForbidden, nil
}
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
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 := runCommand(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
}
// Publish publishes a post.
func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// Before save command handler.
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
// We only run undraft command if it is a file.
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
if err := h.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
}
// Regenerates the file
h.run(false)
// Executed the before publish command.
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// Schedule schedules a post.
func (h Hugo) Schedule(c *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() {
if err := h.undraft(path); err != nil {
log.Printf(err.Error())
}
h.run(false)
})
scheduler.Start()
return http.StatusOK, nil
}
// Preview handles the preview path.
func (h *Hugo) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if h.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
h.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := h.Args
args = append(args, "--baseURL", c.RootURL()+"/preview/")
args = append(args, "--buildDrafts")
args = append(args, "--destination", h.previewPath)
// Builds the preview.
if err := runCommand(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r)
return 0, nil
}
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 := varutils.StringInSlice("--watch", h.Args); b && !force {
if len(h.Args) > pos && h.Args[pos+1] != "false" {
return
}
if len(h.Args) == pos+1 {
return
}
}
if err := runCommand(h.Exe, h.Args, h.Root); err != nil {
log.Println(err)
}
}
func (h Hugo) undraft(file string) error {
args := []string{"undraft", file}
if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") {
return err
}
return nil
}
func (h *Hugo) find() error {
var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil {
return ErrHugoNotFound
}
return nil
}
// runCommand executes an external command
func runCommand(command string, args []string, path string) error {
cmd := exec.Command(command, args...)
cmd.Dir = path
out, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(out))
}
return nil
}

View File

@ -344,10 +344,7 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.Password = suser.Password
}
// Default permissions if current are nil.
if u.Permissions == nil {
u.Permissions = c.DefaultUser.Permissions
}
// Updates the whole User struct because we always are supposed
// to send a new entire object.