mirror of
https://github.com/filebrowser/filebrowser.git
synced 2024-06-07 23:00:43 +00:00
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:
parent
bfdb924cb7
commit
d5e943069e
@ -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;
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
21
assets/src/components/buttons/Schedule.vue
Normal file
21
assets/src/components/buttons/Schedule.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<button @click="show"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')"
|
||||
id="schedule-button"
|
||||
class="action">
|
||||
<i class="material-icons">alarm</i>
|
||||
<span>{{ $t('buttons.schedule') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'schedule-button',
|
||||
methods: {
|
||||
show: function (event) {
|
||||
this.$store.commit('showHover', 'schedule')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -17,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', '')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
68
assets/src/components/prompts/NewArchetype.vue
Normal file
68
assets/src/components/prompts/NewArchetype.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
||||
<p>{{ $t('prompts.newArchetype') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { removePrefix } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'new-archetype',
|
||||
data: function () {
|
||||
return {
|
||||
name: '',
|
||||
archetype: 'default'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
this.new('/' + this.name, this.archetype)
|
||||
.then((url) => {
|
||||
this.$router.push({ path: url })
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
},
|
||||
new (url, type) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
||||
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.getResponseHeader('Location'))
|
||||
} else {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -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,
|
||||
|
41
assets/src/components/prompts/Schedule.vue
Normal file
41
assets/src/components/prompts/Schedule.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.schedule') }}</h3>
|
||||
<p>{{ $t('prompts.scheduleMessage') }}</p>
|
||||
<input autofocus type="datetime-local" v-model="date">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
|
||||
<button class="cancel"
|
||||
@click="close"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'schedule',
|
||||
data: function () {
|
||||
return {
|
||||
date: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
if (this.date === '') return
|
||||
this.close()
|
||||
this.$store.commit('setSchedule', this.date)
|
||||
document.getElementById('save-button').click()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: [],
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
180
filemanager.go
180
filemanager.go
@ -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
62
http.go
@ -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
|
||||
|
258
plugins/hugo.go
258
plugins/hugo.go
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
})()`
|
@ -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
|
||||
}
|
28
resource.go
28
resource.go
@ -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 {
|
||||
|
@ -1 +1 @@
|
||||
824bbb2a05401392205f299b61d082c54ed3f3d2
|
||||
fb7f000455844aaf5021aa2a43f0fcd05f67fa35
|
48
settings.go
48
settings.go
@ -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
239
staticgen.go
Normal 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
|
||||
}
|
5
users.go
5
users.go
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user