StaticGen update Bash

Update zh-cn.yaml (#194)

Address #184

build assets

Update zh-cn.yaml (#194)
pull/196/head
Henrique Dias 2017-08-09 15:06:28 +01:00
parent 9819c87853
commit da4fd84002
No known key found for this signature in database
GPG Key ID: 936F5EB68D786730
28 changed files with 986 additions and 1199 deletions

View File

@ -5,6 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="base" content="{{ .BaseURL }}"> <meta name="base" content="{{ .BaseURL }}">
<meta name="staticgen" content="{{ .StaticGen }}">
<title>File Manager</title> <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="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"> <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)$/)) { %> if (file.match(/\.(js|css)$/)) { %>
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %> <link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<!-- Plugins info -->
<script>{{ .JavaScript }}</script>
<style> <style>
#loading { #loading {
position: fixed; position: fixed;

View File

@ -16,19 +16,11 @@
<i class="material-icons">save</i> <i class="material-icons">save</i>
</button> </button>
<div v-for="plugin in plugins" :key="plugin.name"> <template v-if="staticGen.length > 0">
<button class="action" <button v-show="showPublishButton" :aria-label="$t('buttons.publish')" :title="$t('buttons.publish')" class="action" id="publish-button">
v-for="action in plugin.header.visible" <i class="material-icons">send</i>
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>
</button> </button>
</div> </template>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.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> <i class="material-icons">more_vert</i>
@ -52,19 +44,9 @@
<delete-button v-show="showDeleteButton"></delete-button> <delete-button v-show="showDeleteButton"></delete-button>
</div> </div>
<div v-for="plugin in plugins" :key="plugin.name"> <template v-if="staticGen.length > 0">
<button class="action" <schedule-button v-show="showPublishButton"></schedule-button>
v-for="action in plugin.header.hidden" </template>
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>
<switch-button v-show="showSwitchButton"></switch-button> <switch-button v-show="showSwitchButton"></switch-button>
<download-button v-show="showCommonButton"></download-button> <download-button v-show="showCommonButton"></download-button>
@ -91,6 +73,7 @@ import DownloadButton from './buttons/Download'
import SwitchButton from './buttons/SwitchView' import SwitchButton from './buttons/SwitchView'
import MoveButton from './buttons/Move' import MoveButton from './buttons/Move'
import CopyButton from './buttons/Copy' import CopyButton from './buttons/Copy'
import ScheduleButton from './buttons/Schedule'
import {mapGetters, mapState} from 'vuex' import {mapGetters, mapState} from 'vuex'
import * as api from '@/utils/api' import * as api from '@/utils/api'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
@ -106,7 +89,8 @@ export default {
CopyButton, CopyButton,
UploadButton, UploadButton,
SwitchButton, SwitchButton,
MoveButton MoveButton,
ScheduleButton
}, },
data: function () { data: function () {
return { return {
@ -134,7 +118,7 @@ export default {
'loading', 'loading',
'reload', 'reload',
'multiple', 'multiple',
'plugins' 'staticGen'
]), ]),
isMobile () { isMobile () {
return this.width <= 736 return this.width <= 736
@ -148,6 +132,9 @@ export default {
showSaveButton () { showSaveButton () {
return (this.req.kind === 'editor' && !this.loading) return (this.req.kind === 'editor' && !this.loading)
}, },
showPublishButton () {
return (this.req.kind === 'editor' && !this.loading && this.user.allowPublish)
},
showSwitchButton () { showSwitchButton () {
return this.req.kind === 'listing' && this.$route.name === 'Files' && !this.loading return this.req.kind === 'listing' && this.$route.name === 'Files' && !this.loading
}, },

View File

@ -17,10 +17,32 @@
</button> </button>
</div> </div>
<div v-for="plugin in plugins" :key="plugin.name"> <div v-if="staticGen.length > 0">
<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"> <router-link to="/files/settings"
<i class="material-icons">{{ action.icon }}</i> :aria-label="$t('sidebar.siteSettings')"
<span>{{ action.name }}</span> :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> </button>
</div> </div>
@ -38,7 +60,6 @@
<p class="credits"> <p class="credits">
<span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span> <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> <span><a @click="help">{{ $t('sidebar.help') }}</a></span>
</p> </p>
</nav> </nav>
@ -47,31 +68,22 @@
<script> <script>
import {mapState} from 'vuex' import {mapState} from 'vuex'
import auth from '@/utils/auth' import auth from '@/utils/auth'
import buttons from '@/utils/buttons'
import api from '@/utils/api'
export default { export default {
name: 'sidebar', name: 'sidebar',
data: function () {
return {
pluginData: {
api,
buttons,
'store': this.$store,
'router': this.$router
}
}
},
computed: { computed: {
...mapState(['user', 'plugins']), ...mapState(['user', 'staticGen']),
active () { active () {
return this.$store.state.show === 'sidebar' return this.$store.state.show === 'sidebar'
} }
}, },
methods: { methods: {
help: function () { help () {
this.$store.commit('showHover', 'help') this.$store.commit('showHover', 'help')
}, },
preview () {
window.open(this.$store.state.baseURL + '/preview/')
},
logout: auth.logout 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 { export default {
name: 'editor', name: 'editor',
computed: { computed: {
...mapState(['req']), ...mapState(['req', 'schedule']),
hasMetadata: function () { hasMetadata: function () {
return (this.req.metadata !== undefined && this.req.metadata !== null) return (this.req.metadata !== undefined && this.req.metadata !== null)
} }
@ -32,10 +32,20 @@ export default {
created () { created () {
window.addEventListener('keydown', this.keyEvent) window.addEventListener('keydown', this.keyEvent)
document.getElementById('save-button').addEventListener('click', this.save) document.getElementById('save-button').addEventListener('click', this.save)
let publish = document.getElementById('publish-button')
if (publish !== null) {
publish.addEventListener('click', this.publish)
}
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent) window.removeEventListener('keydown', this.keyEvent)
document.getElementById('save-button').removeEventListener('click', this.save) document.getElementById('save-button').removeEventListener('click', this.save)
let publish = document.getElementById('publish-button')
if (publish !== null) {
publish.removeEventListener('click', this.publish)
}
}, },
mounted: function () { mounted: function () {
if (this.req.content === undefined || this.req.content === null) { if (this.req.content === undefined || this.req.content === null) {
@ -102,22 +112,30 @@ export default {
this.metalang = 'toml' this.metalang = 'toml'
} }
}, },
// Publishes the file.
publish (event) {
this.save(event, true)
},
// Saves the file. // Saves the file.
save () { save (event, regenerate = false) {
buttons.loading('save') let button = regenerate ? 'publish' : 'save'
if (this.schedule !== '') button = 'schedule'
let content = this.content.getValue() let content = this.content.getValue()
buttons.loading(button)
if (this.hasMetadata) { if (this.hasMetadata) {
content = this.metadata.getValue() + '\n\n' + content content = this.metadata.getValue() + '\n\n' + content
} }
api.put(this.$route.path, content) api.put(this.$route.path, content, regenerate, this.schedule)
.then(() => { .then(() => {
buttons.done('save') buttons.done(button)
this.$store.commit('setSchedule', '')
}) })
.catch(error => { .catch(error => {
buttons.done('save') buttons.done(button)
this.$store.commit('showError', error) 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> <error v-else-if="showError"></error>
<success v-else-if="showSuccess"></success> <success v-else-if="showSuccess"></success>
<replace v-else-if="showReplace"></replace> <replace v-else-if="showReplace"></replace>
<schedule v-else-if="show === 'schedule'"></schedule>
<template v-for="plugin in plugins"> <new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
<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>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div> <div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div> </div>
</template> </template>
@ -55,7 +30,9 @@ import Error from './Error'
import Success from './Success' import Success from './Success'
import NewFile from './NewFile' import NewFile from './NewFile'
import NewDir from './NewDir' import NewDir from './NewDir'
import NewArchetype from './NewArchetype'
import Replace from './Replace' import Replace from './Replace'
import Schedule from './Schedule'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
import api from '@/utils/api' import api from '@/utils/api'
@ -65,6 +42,8 @@ export default {
components: { components: {
Info, Info,
Delete, Delete,
NewArchetype,
Schedule,
Rename, Rename,
Error, Error,
Download, 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 save: Save
search: Search search: Search
select: Select select: Select
publish: Publish
selectMultiple: Select multiple selectMultiple: Select multiple
schedule: Schedule
switchView: Swicth view switchView: Swicth view
toggleSidebar: Toggle sidebar toggleSidebar: Toggle sidebar
update: Update update: Update
@ -93,12 +95,16 @@ prompts:
renameMessage: Insert a new name for renameMessage: Insert a new name for
show: Show show: Show
size: Size 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: settings:
admin: Admin admin: Admin
administrator: Administrator administrator: Administrator
allowCommands: Execute commands 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 allowNew: Create new files and directories
allowPublish: Publish new posts and pages
avoidChanges: "(leave blank to avoid changes)" avoidChanges: "(leave blank to avoid changes)"
changePassword: Change Password changePassword: Change Password
commands: Commands commands: Commands
@ -122,7 +128,6 @@ settings:
You can set the user to be an administrator or choose the permissions 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 individually. If you select "Administrator", all of the other options will be
automatically checked. The management of users remains a privilege of an administrator. automatically checked. The management of users remains a privilege of an administrator.
pluginsUpdated: Plugins settings updated!
profileSettings: Profile Settings profileSettings: Profile Settings
ruleExample1: > ruleExample1: >
'prevents the access to any dot file (such as .git, .gitignore) in 'prevents the access to any dot file (such as .git, .gitignore) in
@ -158,6 +163,9 @@ sidebar:
newFolder: New folder newFolder: New folder
servedWith: Served with servedWith: Served with
settings: Settings settings: Settings
siteSettings: Site Settings
hugoNew: Hugo New
preview: Preview
search: search:
images: Images images: Images
music: Music music: Music

View File

@ -14,10 +14,12 @@ buttons:
next: Próximo next: Próximo
ok: OK ok: OK
previous: Anterior previous: Anterior
publish: Publicar
rename: Renomear rename: Renomear
replace: Substituir replace: Substituir
reportIssue: Reportar Erro reportIssue: Reportar Erro
save: Guardar save: Guardar
schedule: Agendar
search: Pesquisar search: Pesquisar
select: Selecionar select: Selecionar
selectMultiple: Selecionar múltiplos selectMultiple: Selecionar múltiplos
@ -30,11 +32,11 @@ errors:
internal: Algo correu bastante mal. internal: Algo correu bastante mal.
notFound: Não conseguimos chegar a esta localização. notFound: Não conseguimos chegar a esta localização.
files: files:
folders: Pastas
files: Ficheiros
body: Corpo body: Corpo
clear: Limpar clear: Limpar
closePreview: Fechar pré-visualização closePreview: Fechar pré-visualização
files: Ficheiros
folders: Pastas
home: Início home: Início
lastModified: Última modificação lastModified: Última modificação
loading: A carregar... loading: A carregar...
@ -43,9 +45,9 @@ files:
multipleSelectionEnabled: Seleção múltipla ativada multipleSelectionEnabled: Seleção múltipla ativada
name: Nome name: Nome
size: Tamanho size: Tamanho
sortByLastModified: Ordenar pela última modificação
sortByName: Ordenar pelo nome sortByName: Ordenar pelo nome
sortBySize: Ordenar pelo tamanho sortBySize: Ordenar pelo tamanho
sortByLastModified: Ordenar pela última modificação
help: help:
click: selecionar pasta ou ficheiro click: selecionar pasta ou ficheiro
ctrl: ctrl:
@ -58,6 +60,10 @@ help:
f1: esta informação f1: esta informação
f2: renomear ficheiro f2: renomear ficheiro
help: Ajuda help: Ajuda
languages:
en: Inglês
pt: Português
zhCN: Chinês (Simplificado)
login: login:
password: Palavra-passe password: Palavra-passe
submit: Login submit: Login
@ -79,6 +85,8 @@ prompts:
lastModified: Última Modificação lastModified: Última Modificação
move: Mover move: Mover
moveMessage: 'Escolha uma nova casa para os seus ficheiros:' 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 newDir: Nova pasta
newDirMessage: Escreva o nome da nova pasta. newDirMessage: Escreva o nome da nova pasta.
newFile: Novo ficheiro newFile: Novo ficheiro
@ -89,22 +97,38 @@ prompts:
renameMessage: Insira um novo nome para renameMessage: Insira um novo nome para
replace: Substituir replace: Substituir
replaceMessage: > replaceMessage: >
Já existe um ficheiro com nome igual a um dos que está a tentar enviar. Deseja Já existe um ficheiro com nome igual a um dos que está a tentar
substituir? enviar. Deseja substituir?
schedule: Agendar
scheduleMessage: Escolha uma data para publicar este post.
show: Mostrar show: Mostrar
size: Tamanho 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: settings:
admin: Admin admin: Admin
administrator: Administrador administrator: Administrador
allowCommands: Executar comandos allowCommands: Executar comandos
allowEdit: Editar, renomear e eliminar ficheiros ou pastas allowEdit: Editar, renomear e eliminar ficheiros ou pastas
allowNew: Criar novos ficheiros e pastas allowNew: Criar novos ficheiros e pastas
allowPublish: Publicar novas páginas e conteúdos
avoidChanges: "(deixe em branco para manter)" avoidChanges: "(deixe em branco para manter)"
changePassword: Alterar Password changePassword: Alterar Password
commands: Comandos commands: Comandos
commandsHelp: > commandsHelp: >
Pode definir um conjunto de comandos a executar em determiandos eventos. Deve Pode definir um conjunto de comandos a executar em determiandos eventos.
escrever um comando por linha. Se o evento estiver relacionado com ficheiros, 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 como antes e depois de guardar, irá existir uma variável de ambiente denominada
"file" com o caminho do ficheiro. "file" com o caminho do ficheiro.
commandsUpdated: Comandos atualizados! commandsUpdated: Comandos atualizados!
@ -119,32 +143,32 @@ settings:
passwordUpdated: Palavra-passe atualizada! passwordUpdated: Palavra-passe atualizada!
permissions: Permissões permissions: Permissões
permissionsHelp: > permissionsHelp: >
Pode definir o utilizador como administrador ou escolher as permissões manualmente. Pode definir o utilizador como administrador ou escolher as permissões
Se selecionar a opção "Administrador", todas as outras opções serão automaticamente manualmente. Se selecionar a opção "Administrador", todas as outras opções serão
selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores. automaticamente selecionadas. A gestão dos utilizadores é um privilégio restringido
pluginsUpdated: Plugins atualizados! aos administradores.
profileSettings: Configurações do Utilizador profileSettings: Configurações do Utilizador
ruleExample1: > 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. ruleExample2: bloqueia o acesso ao ficheiro chamado Caddyfile.
rules: Regras rules: Regras
rulesHelp1: > rulesHelp1: >
Aqui pode definir um conjunto de regras para permitir ou bloquear o acesso Aqui pode definir um conjunto de regras para permitir ou bloquear o
do utilizador a determinados ficheiros ou pastas. Os ficheiros bloqueados não acesso do utilizador a determinados ficheiros ou pastas. Os ficheiros bloqueados
irão aparecer durante a navegação. Suportamos expressões regulares e os caminhos não irão aparecer durante a navegação. Suportamos expressões regulares e os caminhos
dos ficheiros devem ser relativos à base do utilizador. dos ficheiros devem ser relativos à base do utilizador.
rulesHelp2: > rulesHelp2: >
Cada regra deve ser colocada numa linha diferente e deve começar com as Cada regra deve ser colocada numa linha diferente e deve começar com
palavras {0} (permite) ou {1} (bloqueia). Deve escrever, logo de seguida, {2}, 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 caso queira utilizar uma expressão regular. Depois, escreva o caminho do ficheiro/pasta
ou a expressão regular. ou a expressão regular.
scope: Base scope: Base
settingsUpdated: Configurações atualizadas! settingsUpdated: Configurações atualizadas!
user: Utilizador user: Utilizador
userCommands: Comandos userCommands: Comandos
userCommandsHelp: userCommandsHelp: 'Uma lista, separada com espaços, de comandos disponíveis para
'Uma lista, separada com espaços, de comandos disponíveis para este este utilizados. Exemplo:'
utilizados. Exemplo:'
userCreated: Utilizador criado! userCreated: Utilizador criado!
userDeleted: Utilizador eliminado! userDeleted: Utilizador eliminado!
userManagement: Gestão de Utilizadores userManagement: Gestão de Utilizadores
@ -153,26 +177,12 @@ settings:
userUpdated: Utilizador atualizado! userUpdated: Utilizador atualizado!
sidebar: sidebar:
help: Ajuda help: Ajuda
hugoNew: Hugo New
logout: Sair logout: Sair
myFiles: Ficheiros myFiles: Ficheiros
newFile: Novo ficheiro newFile: Novo ficheiro
newFolder: Nova pasta newFolder: Nova pasta
preview: Pré-visualizar
servedWith: Servido com servedWith: Servido com
settings: Configurações settings: Configurações
search: siteSettings: Configurações do Site
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)

View File

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

View File

@ -8,13 +8,14 @@ Vue.use(Vuex)
const state = { const state = {
user: {}, user: {},
req: {}, req: {},
plugins: window.plugins || [],
clipboard: { clipboard: {
key: '', key: '',
items: [] items: []
}, },
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'), baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
jwt: '', jwt: '',
schedule: '',
loading: false, loading: false,
reload: false, reload: false,
selected: [], selected: [],

View File

@ -32,6 +32,9 @@ const mutations = {
setJWT: (state, value) => (state.jwt = value), setJWT: (state, value) => (state.jwt = value),
multiple: (state, value) => (state.multiple = value), multiple: (state, value) => (state.multiple = value),
addSelected: (state, value) => (state.selected.push(value)), addSelected: (state, value) => (state.selected.push(value)),
addPlugin: (state, value) => {
state.plugins.push(value)
},
removeSelected: (state, value) => { removeSelected: (state, value) => {
let i = state.selected.indexOf(value) let i = state.selected.indexOf(value)
if (i === -1) return if (i === -1) return
@ -53,6 +56,9 @@ const mutations = {
resetClipboard: (state) => { resetClipboard: (state) => {
state.clipboard.key = '' state.clipboard.key = ''
state.clipboard.items = [] 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) url = removePrefix(url)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true) request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.setRequestHeader('Publish', publish)
if (date !== '') {
request.setRequestHeader('Schedule', date)
}
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {

View File

@ -15,17 +15,15 @@
<h1>{{ $t('settings.globalSettings') }}</h1> <h1>{{ $t('settings.globalSettings') }}</h1>
<form @submit="savePlugin" v-if="plugins.length > 0"> <form @submit="saveStaticGen" v-if="$store.state.staticGen.length > 0">
<template v-for="plugin in plugins"> <h2>{{ capitalize($store.state.staticGen) }}</h2>
<h2>{{ capitalize(plugin.name) }}</h2>
<p v-for="field in plugin.fields" :key="field.variable"> <p v-for="field in staticGen" :key="field.variable">
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label> <label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value"> <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"> <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> <template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
</p> </p>
</template>
<p><input type="submit" value="Save"></p> <p><input type="submit" value="Save"></p>
</form> </form>
@ -55,7 +53,7 @@ export default {
data: function () { data: function () {
return { return {
commands: [], commands: [],
plugins: [] staticGen: []
} }
}, },
computed: { computed: {
@ -64,8 +62,8 @@ export default {
created () { created () {
getSettings() getSettings()
.then(settings => { .then(settings => {
for (let key in settings.plugins) { if (this.$store.state.staticGen.length > 0) {
this.plugins.push(this.parsePlugin(key, settings.plugins[key])) this.parseStaticGen(settings.staticGen)
} }
for (let key in settings.commands) { for (let key in settings.commands) {
@ -108,40 +106,29 @@ export default {
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) }) .then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
.catch(error => { this.showError(error) }) .catch(error => { this.showError(error) })
}, },
savePlugin (event) { saveStaticGen (event) {
event.preventDefault() event.preventDefault()
let plugins = {} let staticGen = {}
for (let plugin of this.plugins) { for (let field of this.staticGen) {
let p = {} staticGen[field.variable] = field.value
for (let field of plugin.fields) { if (field.original === 'array') {
p[field.variable] = field.value let val = field.value.split(' ')
if (val[0] === '') {
if (field.original === 'array') { val.shift()
let val = field.value.split(' ')
if (val[0] === '') {
val.shift()
}
p[field.variable] = val
} }
}
plugins[plugin.name] = p staticGen[field.variable] = val
}
} }
updateSettings(plugins, 'plugins') updateSettings(staticGen, 'staticGen')
.then(() => { this.showSuccess(this.$t('settings.pluginsUpdated')) }) .then(() => { this.showSuccess(this.$t('settings.settingsUpdated')) })
.catch(error => { this.showError(error) }) .catch(error => { this.showError(error) })
}, },
parsePlugin (name, plugin) { parseStaticGen (staticgen) {
let obj = { for (let option of staticgen) {
name: name,
fields: []
}
for (let option of plugin) {
let value = option.value let value = option.value
let field = { let field = {
@ -156,7 +143,7 @@ export default {
field.original = 'array' field.original = 'array'
field.value = value.join(' ') field.value = value.join(' ')
obj.fields.push(field) this.staticGen.push(field)
continue continue
} }
@ -167,10 +154,8 @@ export default {
break 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="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="allowEdit"> {{ $t('settings.allowEdit') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p> <p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
<p v-for="(value, key) in permissions" :key="key"> <p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
<input type="checkbox" :disabled="admin" v-model="permissions[key]"> {{ capitalize(key) }}
</p>
<h3>{{ $t('settings.userCommands') }}</h3> <h3>{{ $t('settings.userCommands') }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p> <p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
@ -94,6 +92,7 @@ export default {
allowNew: false, allowNew: false,
allowEdit: false, allowEdit: false,
allowCommands: false, allowCommands: false,
allowPublish: false,
permissions: {}, permissions: {},
password: '', password: '',
username: '', username: '',
@ -120,6 +119,7 @@ export default {
this.allowCommands = true this.allowCommands = true
this.allowEdit = true this.allowEdit = true
this.allowNew = true this.allowNew = true
this.allowPublish = true
for (let key in this.permissions) { for (let key in this.permissions) {
this.permissions[key] = true this.permissions[key] = true
} }
@ -140,6 +140,7 @@ export default {
this.allowCommands = user.allowCommands this.allowCommands = user.allowCommands
this.allowNew = user.allowNew this.allowNew = user.allowNew
this.allowEdit = user.allowEdit this.allowEdit = user.allowEdit
this.allowPublish = user.allowPublish
this.filesystem = user.filesystem this.filesystem = user.filesystem
this.username = user.username this.username = user.username
this.commands = user.commands.join(' ') this.commands = user.commands.join(' ')
@ -183,6 +184,7 @@ export default {
this.admin = false this.admin = false
this.allowNew = false this.allowNew = false
this.allowEdit = false this.allowEdit = false
this.allowPublish = false
this.permissins = {} this.permissins = {}
this.allowCommands = false this.allowCommands = false
this.password = '' this.password = ''
@ -241,6 +243,7 @@ export default {
allowCommands: this.allowCommands, allowCommands: this.allowCommands,
allowNew: this.allowNew, allowNew: this.allowNew,
allowEdit: this.allowEdit, allowEdit: this.allowEdit,
allowPublish: this.allowPublish,
permissions: this.permissions, permissions: this.permissions,
css: this.css, css: this.css,
locale: this.locale, locale: this.locale,

View File

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

View File

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

View File

@ -78,7 +78,6 @@ var (
errEmptyScope = errors.New("scope is empty") errEmptyScope = errors.New("scope is empty")
errWrongDataType = errors.New("wrong data type") errWrongDataType = errors.New("wrong data type")
errInvalidUpdateField = errors.New("invalid field to update") errInvalidUpdateField = errors.New("invalid field to update")
plugins = map[string]Plugin{}
) )
// FileManager is a file manager instance. It should be creating using the // 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". // there will only exist one user, called "admin".
NoAuth bool 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. // The Default User needed to build the New User page.
DefaultUser *User DefaultUser *User
@ -115,9 +119,15 @@ type FileManager struct {
// A map of events to a slice of commands. // A map of events to a slice of commands.
Commands map[string][]string Commands map[string][]string
}
// The options of the plugins that have been plugged into this instance. type StaticGen interface {
Plugins map[string]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. // Command is a command function.
@ -151,10 +161,10 @@ type User struct {
Locale string `json:"locale"` Locale string `json:"locale"`
// These indicate if the user can perform certain actions. // These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands AllowCommands bool `json:"allowCommands"` // Execute commands
Permissions map[string]bool `json:"permissions"` // Permissions added by plugins AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute. // Commands is the list of commands the user can execute.
Commands []string `json:"commands"` Commands []string `json:"commands"`
@ -175,43 +185,18 @@ type Rule struct {
Regexp *Regexp `json:"regexp"` 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. // Regexp is a regular expression wrapper around native regexp.
type Regexp struct { type Regexp struct {
Raw string `json:"raw"` Raw string `json:"raw"`
regexp *regexp.Regexp 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. // DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{ var DefaultUser = User{
AllowCommands: true, AllowCommands: true,
AllowEdit: true, AllowEdit: true,
AllowNew: true, AllowNew: true,
Permissions: map[string]bool{}, AllowPublish: true,
Commands: []string{}, Commands: []string{},
Rules: []*Rule{}, Rules: []*Rule{},
CSS: "", CSS: "",
@ -228,9 +213,8 @@ func New(database string, base User) (*FileManager, error) {
// Creates a new File Manager instance with the Users // Creates a new File Manager instance with the Users
// map and Assets box. // map and Assets box.
m := &FileManager{ m := &FileManager{
Users: map[string]*User{}, Users: map[string]*User{},
Plugins: map[string]interface{}{}, assets: rice.MustFindBox("./assets/dist"),
assets: rice.MustFindBox("./assets/dist"),
} }
// Tries to open a database on the location provided. This // 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) err = db.Get("config", "commands", &m.Commands)
if err != nil && err == storm.ErrNotFound { if err != nil && err == storm.ErrNotFound {
m.Commands = map[string][]string{ m.Commands = map[string][]string{
"before_save": {}, "before_save": {},
"after_save": {}, "after_save": {},
"before_publish": {},
"after_publish": {},
} }
err = db.Set("config", "commands", m.Commands) err = db.Set("config", "commands", m.Commands)
} }
@ -346,95 +332,7 @@ func (m *FileManager) SetBaseURL(url string) {
m.BaseURL = strings.TrimSuffix(url, "/") m.BaseURL = strings.TrimSuffix(url, "/")
} }
// ActivatePlugin activates a plugin to a File Manager instance and // ServeHTTP handles the request.
// 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.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := serveHTTP(&RequestContext{ code, err := serveHTTP(&RequestContext{
FileManager: m, 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. // Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool { func (u User) Allowed(url string) bool {
var rule *Rule 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) return apiHandler(c, w, r)
} }
// Checks if any plugin has an handler for this URL. // If it is a request to the preview and a static website generator is
for p := range c.Plugins { // active, build the preview.
var h PluginHandler if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview")
for path, handler := range plugins[p].Handlers { return c.StaticGen.Preview(c, w, r)
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)
} }
// Any other request should show the index.html file. // 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 return http.StatusForbidden, nil
} }
for p := range c.Plugins { if c.StaticGen != nil {
if plugins[p].BeforeAPI == nil { // If we are using the 'magic url' for the settings,
continue // 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 { if code != 0 || err != nil {
return code, err return code, err
} }
@ -172,21 +158,6 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
code = http.StatusNotFound 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 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)) tpl := template.Must(template.New("file").Parse(file))
w.Header().Set("Content-Type", contentType+"; charset=utf-8") 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{}{ err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(), "BaseURL": c.RootURL(),
"JavaScript": template.JS(javascript), "StaticGen": c.staticgen,
}) })
if err != nil { if err != nil {
return http.StatusInternalServerError, err 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 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. // Writes the ETag Header.
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()) etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
w.Header().Set("ETag", etag) w.Header().Set("ETag", etag)
return http.StatusOK, nil 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. // resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit { if !c.User.AllowEdit {

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
package filemanager package filemanager
import ( import (
"bytes"
"encoding/json" "encoding/json"
"net/http" "net/http"
"reflect" "reflect"
@ -11,12 +12,12 @@ import (
type modifySettingsRequest struct { type modifySettingsRequest struct {
*modifyRequest *modifyRequest
Data struct { Data struct {
Commands map[string][]string `json:"commands"` Commands map[string][]string `json:"commands"`
Plugins map[string]map[string]interface{} `json:"plugins"` StaticGen map[string]interface{} `json:"staticGen"`
} `json:"data"` } `json:"data"`
} }
type pluginOption struct { type option struct {
Variable string `json:"variable"` Variable string `json:"variable"`
Name string `json:"name"` Name string `json:"name"`
Value interface{} `json:"value"` Value interface{} `json:"value"`
@ -59,8 +60,8 @@ func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
type settingsGetRequest struct { type settingsGetRequest struct {
Commands map[string][]string `json:"commands"` Commands map[string][]string `json:"commands"`
Plugins map[string][]pluginOption `json:"plugins"` StaticGen []option `json:"staticGen"`
} }
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { 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{ result := &settingsGetRequest{
Commands: c.Commands, Commands: c.Commands,
Plugins: map[string][]pluginOption{}, StaticGen: []option{},
} }
for name, p := range c.Plugins { if c.StaticGen != nil {
result.Plugins[name] = []pluginOption{} t := reflect.TypeOf(c.StaticGen).Elem()
t := reflect.TypeOf(p).Elem()
for i := 0; i < t.NumField(); i++ { 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, Variable: t.Field(i).Name,
Name: t.Field(i).Tag.Get("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 return http.StatusOK, nil
} }
// Update the plugins. // Update the static generator options.
if mod.Which == "plugins" { if mod.Which == "staticGen" {
for name, plugin := range mod.Data.Plugins { err = mapstructure.Decode(mod.Data.StaticGen, c.StaticGen)
err = mapstructure.Decode(plugin, c.Plugins[name]) if err != nil {
if err != nil { return http.StatusInternalServerError, err
return http.StatusInternalServerError, err }
}
err = c.db.Set("plugins", name, c.Plugins[name]) err = c.db.Set("staticgen", c.staticgen, c.StaticGen)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
}
} }
return http.StatusOK, nil 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 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 // Updates the whole User struct because we always are supposed
// to send a new entire object. // to send a new entire object.