change new folder and file permissions #190
progresses on sharing #192 Progresses on #192 Progresses on #192 Little API update Build assets Former-commit-id: 68e70132ea857eb65638c0496c030be1c181ed1c [formerly d67b74280b7f12c3e20de6abe31fcfc26e8f43ef] [formerly 8fe91e003c9616da23f0e673ad4bb89d792a41c8 [formerly 868434360592aa0280e0d631840750d53a564cd3]] Former-commit-id: 7d22ff468e580601d0c3e0921734b587b92484f8 [formerly 55f9d830636f9bbf15e0453d1ee7de6ee5d5191e] Former-commit-id: ad411a5979521dda9ea9683d86e4c8ae7b3c9e6f
This commit is contained in:
		
							parent
							
								
									25a86a9382
								
							
						
					
					
						commit
						8d715bb433
					
				| 
						 | 
				
			
			@ -9,7 +9,7 @@
 | 
			
		|||
  <title>File Manager</title>
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
 | 
			
		||||
  <!--[if IE]><link rel="shortcut icon" href="/static/img/icons/favicon.ico"><![endif]-->
 | 
			
		||||
  <!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
 | 
			
		||||
  <!-- Add to home screen for Android and modern mobile browsers -->
 | 
			
		||||
  <link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
 | 
			
		||||
  <meta name="theme-color" content="#2979ff">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,7 @@
 | 
			
		|||
      <!-- Menu that shows on listing AND mobile when there are files selected -->
 | 
			
		||||
      <div id="file-selection" v-if="isMobile && req.kind === 'listing'">
 | 
			
		||||
        <span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
 | 
			
		||||
        <share-button v-show="showRenameButton"></share-button>
 | 
			
		||||
        <rename-button v-show="showRenameButton"></rename-button>
 | 
			
		||||
        <copy-button v-show="showMoveButton"></copy-button>
 | 
			
		||||
        <move-button v-show="showMoveButton"></move-button>
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +39,7 @@
 | 
			
		|||
      <!-- This buttons are shown on a dropdown on mobile phones -->
 | 
			
		||||
      <div id="dropdown" :class="{ active: showMore }">
 | 
			
		||||
        <div v-if="!isListing || !isMobile">
 | 
			
		||||
          <share-button v-show="showRenameButton"></share-button>
 | 
			
		||||
          <rename-button v-show="showRenameButton"></rename-button>
 | 
			
		||||
          <copy-button v-show="showMoveButton"></copy-button>
 | 
			
		||||
          <move-button v-show="showMoveButton"></move-button>
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +76,7 @@ import SwitchButton from './buttons/SwitchView'
 | 
			
		|||
import MoveButton from './buttons/Move'
 | 
			
		||||
import CopyButton from './buttons/Copy'
 | 
			
		||||
import ScheduleButton from './buttons/Schedule'
 | 
			
		||||
import ShareButton from './buttons/Share'
 | 
			
		||||
import {mapGetters, mapState} from 'vuex'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
import buttons from '@/utils/buttons'
 | 
			
		||||
| 
						 | 
				
			
			@ -84,6 +87,7 @@ export default {
 | 
			
		|||
    Search,
 | 
			
		||||
    InfoButton,
 | 
			
		||||
    DeleteButton,
 | 
			
		||||
    ShareButton,
 | 
			
		||||
    RenameButton,
 | 
			
		||||
    DownloadButton,
 | 
			
		||||
    CopyButton,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,7 +82,7 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { mapState } from 'vuex'
 | 
			
		||||
import url from '@/utils/url'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'search',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@
 | 
			
		|||
 | 
			
		||||
<script>
 | 
			
		||||
import {mapGetters, mapState} from 'vuex'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'download-button',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action">
 | 
			
		||||
    <i class="material-icons">share</i>
 | 
			
		||||
    <span>{{ $t('buttons.share') }}</span>
 | 
			
		||||
  </button>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'share-button',
 | 
			
		||||
  methods: {
 | 
			
		||||
    show (event) {
 | 
			
		||||
      this.$store.commit('showHover', 'share')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +11,7 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { mapState } from 'vuex'
 | 
			
		||||
import CodeMirror from '@/utils/codemirror'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
import buttons from '@/utils/buttons'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
| 
						 | 
				
			
			@ -129,7 +129,7 @@ export default {
 | 
			
		|||
 | 
			
		||||
      api.put(this.$route.path, content, regenerate, this.schedule)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          buttons.done(button)
 | 
			
		||||
          buttons.success(button)
 | 
			
		||||
          this.$store.commit('setSchedule', '')
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,7 +91,7 @@
 | 
			
		|||
import {mapState} from 'vuex'
 | 
			
		||||
import Item from './ListingItem'
 | 
			
		||||
import css from '@/utils/css'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
import buttons from '@/utils/buttons'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
| 
						 | 
				
			
			@ -325,7 +325,7 @@ export default {
 | 
			
		|||
 | 
			
		||||
      Promise.all(promises)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          buttons.done('upload')
 | 
			
		||||
          buttons.success('upload')
 | 
			
		||||
          this.$store.commit('setReload', true)
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,7 +33,7 @@
 | 
			
		|||
import { mapMutations, mapGetters, mapState } from 'vuex'
 | 
			
		||||
import filesize from 'filesize'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'item',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,7 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { mapState } from 'vuex'
 | 
			
		||||
import url from '@/utils/url'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
import InfoButton from '@/components/buttons/Info'
 | 
			
		||||
import DeleteButton from '@/components/buttons/Delete'
 | 
			
		||||
import RenameButton from '@/components/buttons/Rename'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { mapState } from 'vuex'
 | 
			
		||||
import FileList from './FileList'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
import buttons from '@/utils/buttons'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +51,7 @@ export default {
 | 
			
		|||
      // Execute the promises.
 | 
			
		||||
      api.copy(items)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          buttons.done('copy')
 | 
			
		||||
          buttons.success('copy')
 | 
			
		||||
          this.$router.push({ path: this.dest })
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@
 | 
			
		|||
 | 
			
		||||
<script>
 | 
			
		||||
import {mapGetters, mapMutations, mapState} from 'vuex'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import { remove } from '@/utils/api'
 | 
			
		||||
import url from '@/utils/url'
 | 
			
		||||
import buttons from '@/utils/buttons'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -36,9 +36,9 @@ export default {
 | 
			
		|||
      // If we are not on a listing, delete the current
 | 
			
		||||
      // opened file.
 | 
			
		||||
      if (this.req.kind !== 'listing') {
 | 
			
		||||
        api.delete(this.$route.path)
 | 
			
		||||
        remove(this.$route.path)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            buttons.done('delete')
 | 
			
		||||
            buttons.success('delete')
 | 
			
		||||
            this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
 | 
			
		||||
          })
 | 
			
		||||
          .catch(error => {
 | 
			
		||||
| 
						 | 
				
			
			@ -59,12 +59,12 @@ export default {
 | 
			
		|||
      let promises = []
 | 
			
		||||
 | 
			
		||||
      for (let index of this.selected) {
 | 
			
		||||
        promises.push(api.delete(this.req.items[index].url))
 | 
			
		||||
        promises.push(remove(this.req.items[index].url))
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      Promise.all(promises)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          buttons.done('delete')
 | 
			
		||||
          buttons.success('delete')
 | 
			
		||||
          this.$store.commit('setReload', true)
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@
 | 
			
		|||
 | 
			
		||||
<script>
 | 
			
		||||
import {mapGetters, mapState} from 'vuex'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'download',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { mapState } from 'vuex'
 | 
			
		||||
import url from '@/utils/url'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'file-list',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,7 +34,7 @@
 | 
			
		|||
import {mapState, mapGetters} from 'vuex'
 | 
			
		||||
import filesize from 'filesize'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'info',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { mapState } from 'vuex'
 | 
			
		||||
import FileList from './FileList'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
import buttons from '@/utils/buttons'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +51,7 @@ export default {
 | 
			
		|||
      // Execute the promises.
 | 
			
		||||
      api.move(items)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          buttons.done('move')
 | 
			
		||||
          buttons.success('move')
 | 
			
		||||
          this.$router.push({ path: this.dest })
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@
 | 
			
		|||
 | 
			
		||||
<script>
 | 
			
		||||
import url from '@/utils/url'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'new-dir',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@
 | 
			
		|||
 | 
			
		||||
<script>
 | 
			
		||||
import url from '@/utils/url'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'new-file',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@
 | 
			
		|||
    <replace v-else-if="showReplace"></replace>
 | 
			
		||||
    <schedule v-else-if="show === 'schedule'"></schedule>
 | 
			
		||||
    <new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
 | 
			
		||||
    <share v-else-if="show === 'share'"></share>
 | 
			
		||||
    <div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -33,9 +34,10 @@ import NewDir from './NewDir'
 | 
			
		|||
import NewArchetype from './NewArchetype'
 | 
			
		||||
import Replace from './Replace'
 | 
			
		||||
import Schedule from './Schedule'
 | 
			
		||||
import Share from './Share'
 | 
			
		||||
import { mapState } from 'vuex'
 | 
			
		||||
import buttons from '@/utils/buttons'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'prompts',
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +52,7 @@ export default {
 | 
			
		|||
    Success,
 | 
			
		||||
    Move,
 | 
			
		||||
    Copy,
 | 
			
		||||
    Share,
 | 
			
		||||
    NewFile,
 | 
			
		||||
    NewDir,
 | 
			
		||||
    Help,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { mapState } from 'vuex'
 | 
			
		||||
import url from '@/utils/url'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'rename',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,153 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="prompt" id="share">
 | 
			
		||||
    <h3>{{ $t('buttons.share') }}</h3>
 | 
			
		||||
    <p></p>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li v-if="!hasPermanent">
 | 
			
		||||
        <a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
 | 
			
		||||
      </li>
 | 
			
		||||
 | 
			
		||||
      <li v-for="link in links" :key="link.hash">
 | 
			
		||||
        <a :href="buildLink(link.hash)" target="_blank">
 | 
			
		||||
          <template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
 | 
			
		||||
          <template v-else>{{ $t('permanent') }}</template>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <button class="action"
 | 
			
		||||
          @click="deleteLink($event, link)"
 | 
			
		||||
          :aria-label="$t('buttons.delete')"
 | 
			
		||||
          :title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
 | 
			
		||||
 | 
			
		||||
        <button class="action copy"
 | 
			
		||||
          :data-clipboard-text="buildLink(link.hash)"
 | 
			
		||||
          :aria-label="$t('buttons.copyToClipboard')"
 | 
			
		||||
          :title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
 | 
			
		||||
      </li>
 | 
			
		||||
 | 
			
		||||
      <li>
 | 
			
		||||
        <input autofocus
 | 
			
		||||
          type="number"
 | 
			
		||||
          max="2147483647"
 | 
			
		||||
          min="0"
 | 
			
		||||
          @keyup.enter="submit"
 | 
			
		||||
          v-model.trim="time">
 | 
			
		||||
        <select v-model="unit" :aria-label="$t('time.unit')">
 | 
			
		||||
          <option value="seconds">{{ $t('time.seconds') }}</option>
 | 
			
		||||
          <option value="minutes">{{ $t('time.minutes') }}</option>
 | 
			
		||||
          <option value="hours">{{ $t('time.hours') }}</option>
 | 
			
		||||
          <option value="days">{{ $t('time.days') }}</option>
 | 
			
		||||
        </select>
 | 
			
		||||
        <button class="action"
 | 
			
		||||
          @click="submit"
 | 
			
		||||
          :aria-label="$t('buttons.create')"
 | 
			
		||||
          :title="$t('buttons.create')"><i class="material-icons">add</i></button>
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
      <button class="cancel"
 | 
			
		||||
        @click="$store.commit('closeHovers')"
 | 
			
		||||
        :aria-label="$t('buttons.close')"
 | 
			
		||||
        :title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapState, mapMutations } from 'vuex'
 | 
			
		||||
import { getShare, deleteShare, share } from '@/utils/api'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
import Clipboard from 'clipboard'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'share',
 | 
			
		||||
  data: function () {
 | 
			
		||||
    return {
 | 
			
		||||
      time: '',
 | 
			
		||||
      unit: 'hours',
 | 
			
		||||
      hasPermanent: false,
 | 
			
		||||
      links: [],
 | 
			
		||||
      clip: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapState([ 'baseURL', 'req', 'selected', 'selectedCount' ]),
 | 
			
		||||
    url () {
 | 
			
		||||
      // Get the current name of the file we are editing.
 | 
			
		||||
      if (this.req.kind !== 'listing') {
 | 
			
		||||
        return this.$route.path
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.selectedCount === 0 || this.selectedCount > 1) {
 | 
			
		||||
        // This shouldn't happen.
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return this.req.items[this.selected[0]].url
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeMount () {
 | 
			
		||||
    getShare(this.url)
 | 
			
		||||
      .then(links => {
 | 
			
		||||
        this.links = links
 | 
			
		||||
        this.sort()
 | 
			
		||||
 | 
			
		||||
        for (let link of this.links) {
 | 
			
		||||
          if (!link.expires) {
 | 
			
		||||
            this.hasPermanent = true
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => {
 | 
			
		||||
        if (error === 404) return
 | 
			
		||||
        this.showError(error)
 | 
			
		||||
      })
 | 
			
		||||
  },
 | 
			
		||||
  mounted () {
 | 
			
		||||
    this.clip = new Clipboard('.copy')
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapMutations([ 'showError' ]),
 | 
			
		||||
    submit: function (event) {
 | 
			
		||||
      if (!this.time) return
 | 
			
		||||
 | 
			
		||||
      share(this.url, this.time, this.unit)
 | 
			
		||||
        .then(result => { this.links.push(result); this.sort() })
 | 
			
		||||
        .catch(error => { this.showError(error) })
 | 
			
		||||
    },
 | 
			
		||||
    getPermalink (event) {
 | 
			
		||||
      share(this.url)
 | 
			
		||||
        .then(result => {
 | 
			
		||||
          this.links.push(result)
 | 
			
		||||
          this.sort()
 | 
			
		||||
          this.hasPermanent = true
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => { this.showError(error) })
 | 
			
		||||
    },
 | 
			
		||||
    deleteLink (event, link) {
 | 
			
		||||
      event.preventDefault()
 | 
			
		||||
      deleteShare(link.hash)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          if (!link.expires) this.hasPermanent = false
 | 
			
		||||
          this.links = this.links.filter(item => item.hash !== link.hash)
 | 
			
		||||
        })
 | 
			
		||||
        .catch(error => { this.showError(error) })
 | 
			
		||||
    },
 | 
			
		||||
    humanTime (time) {
 | 
			
		||||
      return moment(time).fromNow()
 | 
			
		||||
    },
 | 
			
		||||
    buildLink (hash) {
 | 
			
		||||
      return `${window.location.origin}${this.baseURL}/share/${hash}`
 | 
			
		||||
    },
 | 
			
		||||
    sort () {
 | 
			
		||||
      this.links = this.links.sort((a, b) => {
 | 
			
		||||
        if (!a.expires) return -1
 | 
			
		||||
        if (!b.expires) return 1
 | 
			
		||||
        return new Date(a.expireDate) - new Date(b.expireDate)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +61,7 @@
 | 
			
		|||
    background: #fff;
 | 
			
		||||
    box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
 | 
			
		||||
    width: 95%;
 | 
			
		||||
    max-width: 18em;
 | 
			
		||||
    max-width: 20em;
 | 
			
		||||
  }
 | 
			
		||||
  #file-selection .action {
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -177,3 +177,32 @@
 | 
			
		|||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.prompt#share ul {
 | 
			
		||||
  list-style: none;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.prompt#share ul li {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.prompt#share ul li a {
 | 
			
		||||
  color: #2196F3;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.prompt#share ul li .action i {
 | 
			
		||||
  font-size: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.prompt#share ul li input,
 | 
			
		||||
.prompt#share ul li select {
 | 
			
		||||
  padding: .2em;
 | 
			
		||||
  margin-right: .5em;
 | 
			
		||||
  border: 1px solid #dadada;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,10 @@
 | 
			
		|||
permanent: Permanent
 | 
			
		||||
buttons:
 | 
			
		||||
  cancel: Cancel
 | 
			
		||||
  close: Close
 | 
			
		||||
  copy: Copy
 | 
			
		||||
  copyFile: Copy file
 | 
			
		||||
  copyToClipboard: Copy to clipboard
 | 
			
		||||
  create: Create
 | 
			
		||||
  delete: Delete
 | 
			
		||||
  download: Download
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +22,7 @@ buttons:
 | 
			
		|||
  save: Save
 | 
			
		||||
  search: Search
 | 
			
		||||
  select: Select
 | 
			
		||||
  share: Share
 | 
			
		||||
  publish: Publish
 | 
			
		||||
  selectMultiple: Select multiple
 | 
			
		||||
  schedule: Schedule
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +30,7 @@ buttons:
 | 
			
		|||
  toggleSidebar: Toggle sidebar
 | 
			
		||||
  update: Update
 | 
			
		||||
  upload: Upload
 | 
			
		||||
  permalink: Get Permanent Link
 | 
			
		||||
errors:
 | 
			
		||||
  forbidden: You're not welcome here.
 | 
			
		||||
  internal: Something really went wrong.
 | 
			
		||||
| 
						 | 
				
			
			@ -183,3 +187,9 @@ languages:
 | 
			
		|||
  en: English
 | 
			
		||||
  pt: Portuguese
 | 
			
		||||
  zhCN: Chinese (Simplified)
 | 
			
		||||
time:
 | 
			
		||||
  unit: Time Unit
 | 
			
		||||
  seconds: Seconds
 | 
			
		||||
  minutes: Minutes
 | 
			
		||||
  hours: Hours
 | 
			
		||||
  days: Days
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,10 @@
 | 
			
		|||
permanent: Permanente
 | 
			
		||||
buttons:
 | 
			
		||||
  cancel: Cancelar
 | 
			
		||||
  close: Fechar
 | 
			
		||||
  copy: Copiar
 | 
			
		||||
  copyFile: Copiar ficheiro
 | 
			
		||||
  copyToClipboard: Copiar
 | 
			
		||||
  create: Criar
 | 
			
		||||
  delete: Eliminar
 | 
			
		||||
  download: Descarregar
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +21,7 @@ buttons:
 | 
			
		|||
  replace: Substituir
 | 
			
		||||
  reportIssue: Reportar Erro
 | 
			
		||||
  save: Guardar
 | 
			
		||||
  share: Partilhar
 | 
			
		||||
  schedule: Agendar
 | 
			
		||||
  search: Pesquisar
 | 
			
		||||
  select: Selecionar
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +30,7 @@ buttons:
 | 
			
		|||
  toggleSidebar: Alternar barra lateral
 | 
			
		||||
  update: Atualizar
 | 
			
		||||
  upload: Enviar
 | 
			
		||||
  permalink: Obter link permanente
 | 
			
		||||
errors:
 | 
			
		||||
  forbidden: Tu não és bem-vindo aqui.
 | 
			
		||||
  internal: Algo correu bastante mal.
 | 
			
		||||
| 
						 | 
				
			
			@ -186,3 +190,9 @@ sidebar:
 | 
			
		|||
  servedWith: Servido com
 | 
			
		||||
  settings: Configurações
 | 
			
		||||
  siteSettings: Configurações do Site
 | 
			
		||||
time:
 | 
			
		||||
  unit: Unidades de Tempo
 | 
			
		||||
  seconds: Segundos
 | 
			
		||||
  minutes: Minutos
 | 
			
		||||
  hours: Horas
 | 
			
		||||
  days: Dias
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import i18n from '@/i18n'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
 | 
			
		||||
const mutations = {
 | 
			
		||||
  closeHovers: state => {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +27,7 @@ const mutations = {
 | 
			
		|||
  setLoading: (state, value) => { state.loading = value },
 | 
			
		||||
  setReload: (state, value) => { state.reload = value },
 | 
			
		||||
  setUser: (state, value) => {
 | 
			
		||||
    moment.locale(value.locale)
 | 
			
		||||
    i18n.locale = value.locale
 | 
			
		||||
    state.user = value
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,7 @@ export function fetch (url) {
 | 
			
		|||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function rm (url) {
 | 
			
		||||
export function remove (url) {
 | 
			
		||||
  url = removePrefix(url)
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -383,25 +383,69 @@ export function deleteUser (id) {
 | 
			
		|||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  removePrefix,
 | 
			
		||||
  delete: rm,
 | 
			
		||||
  fetch,
 | 
			
		||||
  checksum,
 | 
			
		||||
  move,
 | 
			
		||||
  put,
 | 
			
		||||
  copy,
 | 
			
		||||
  post,
 | 
			
		||||
  command,
 | 
			
		||||
  search,
 | 
			
		||||
  download,
 | 
			
		||||
  // other things
 | 
			
		||||
  getSettings,
 | 
			
		||||
  updateSettings,
 | 
			
		||||
  // User things
 | 
			
		||||
  newUser,
 | 
			
		||||
  getUser,
 | 
			
		||||
  getUsers,
 | 
			
		||||
  updateUser,
 | 
			
		||||
  deleteUser
 | 
			
		||||
// SHARE
 | 
			
		||||
 | 
			
		||||
export function getShare (url) {
 | 
			
		||||
  url = removePrefix(url)
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    let request = new window.XMLHttpRequest()
 | 
			
		||||
    request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
 | 
			
		||||
    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
 | 
			
		||||
 | 
			
		||||
    request.onload = () => {
 | 
			
		||||
      if (request.status === 200) {
 | 
			
		||||
        resolve(JSON.parse(request.responseText))
 | 
			
		||||
      } else {
 | 
			
		||||
        reject(request.status)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    request.onerror = (error) => reject(error)
 | 
			
		||||
    request.send()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function deleteShare (hash) {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    let request = new window.XMLHttpRequest()
 | 
			
		||||
    request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
 | 
			
		||||
    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
 | 
			
		||||
 | 
			
		||||
    request.onload = () => {
 | 
			
		||||
      if (request.status === 200) {
 | 
			
		||||
        resolve()
 | 
			
		||||
      } else {
 | 
			
		||||
        reject(request.status)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    request.onerror = (error) => reject(error)
 | 
			
		||||
    request.send()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function share (url, expires = '', unit = 'hours') {
 | 
			
		||||
  url = removePrefix(url)
 | 
			
		||||
  url = `${store.state.baseURL}/api/share${url}`
 | 
			
		||||
  if (expires !== '') {
 | 
			
		||||
    url += `?expires=${expires}&unit=${unit}`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    let request = new window.XMLHttpRequest()
 | 
			
		||||
    request.open('POST', url, true)
 | 
			
		||||
    request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
 | 
			
		||||
 | 
			
		||||
    request.onload = () => {
 | 
			
		||||
      if (request.status === 200) {
 | 
			
		||||
        resolve(JSON.parse(request.responseText))
 | 
			
		||||
      } else {
 | 
			
		||||
        reject(request.responseStatus)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    request.onerror = (error) => reject(error)
 | 
			
		||||
    request.send()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ function loading (button) {
 | 
			
		|||
  }, 100)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function done (button, success = true) {
 | 
			
		||||
function done (button) {
 | 
			
		||||
  let el = document.querySelector(`#${button}-button > i`)
 | 
			
		||||
 | 
			
		||||
  if (el === undefined || el === null) {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,34 @@ function done (button, success = true) {
 | 
			
		|||
  }, 100)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function success (button) {
 | 
			
		||||
  let el = document.querySelector(`#${button}-button > i`)
 | 
			
		||||
 | 
			
		||||
  if (el === undefined || el === null) {
 | 
			
		||||
    console.log('Error getting button ' + button)
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  el.style.opacity = 0
 | 
			
		||||
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    el.classList.remove('spin')
 | 
			
		||||
    el.innerHTML = 'done'
 | 
			
		||||
    el.style.opacity = 1
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      el.style.opacity = 0
 | 
			
		||||
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        el.innerHTML = el.dataset.icon
 | 
			
		||||
        el.style.opacity = 1
 | 
			
		||||
      }, 100)
 | 
			
		||||
    }, 500)
 | 
			
		||||
  }, 100)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  loading,
 | 
			
		||||
  done
 | 
			
		||||
  done,
 | 
			
		||||
  success
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ import InternalError from './errors/500'
 | 
			
		|||
import Preview from '@/components/files/Preview'
 | 
			
		||||
import Listing from '@/components/files/Listing'
 | 
			
		||||
import Editor from '@/components/files/Editor'
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
import { mapGetters, mapState, mapMutations } from 'vuex'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,7 @@
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import api from '@/utils/api'
 | 
			
		||||
import * as api from '@/utils/api'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'users',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
 | 
			
		||||
  <title>File Manager</title>
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
 | 
			
		||||
  <!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
 | 
			
		||||
  <link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
 | 
			
		||||
  <meta name="theme-color" content="#2979ff">
 | 
			
		||||
  <meta name="apple-mobile-web-app-capable" content="yes">
 | 
			
		||||
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
 | 
			
		||||
  <meta name="apple-mobile-web-app-title" content="assets">
 | 
			
		||||
  <link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
 | 
			
		||||
  <meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
 | 
			
		||||
  <meta name="msapplication-TileColor" content="#2979ff">
 | 
			
		||||
 | 
			
		||||
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
 | 
			
		||||
  <style>
 | 
			
		||||
    * {
 | 
			
		||||
      box-sizing: border-box
 | 
			
		||||
    }
 | 
			
		||||
    body {
 | 
			
		||||
      font-family: Arial, sans-serif;
 | 
			
		||||
      color: #6f6f6f;
 | 
			
		||||
      background: #f8f8f8;
 | 
			
		||||
    }
 | 
			
		||||
    body > div  {
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      transform: translate(-50%, -50%);
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      left: 50%;
 | 
			
		||||
      box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
 | 
			
		||||
      background: #fff;
 | 
			
		||||
      display: block;
 | 
			
		||||
      border-radius: 0.2em;
 | 
			
		||||
      padding: 2em 3em;
 | 
			
		||||
    }
 | 
			
		||||
    body > a * {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <div><h1>404 Not Found</h1></div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
 | 
			
		||||
  <title>{{ .File.Name }}</title>
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
 | 
			
		||||
  <link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
 | 
			
		||||
  <!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
 | 
			
		||||
  <link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
 | 
			
		||||
  <meta name="theme-color" content="#2979ff">
 | 
			
		||||
  <meta name="apple-mobile-web-app-capable" content="yes">
 | 
			
		||||
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
 | 
			
		||||
  <meta name="apple-mobile-web-app-title" content="assets">
 | 
			
		||||
  <link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
 | 
			
		||||
  <meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
 | 
			
		||||
  <meta name="msapplication-TileColor" content="#2979ff">
 | 
			
		||||
 | 
			
		||||
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
 | 
			
		||||
  <style>
 | 
			
		||||
    * {
 | 
			
		||||
      box-sizing: border-box
 | 
			
		||||
    }
 | 
			
		||||
    body {
 | 
			
		||||
      font-family: Arial, sans-serif;
 | 
			
		||||
      color: #6f6f6f;
 | 
			
		||||
      background: #f8f8f8;
 | 
			
		||||
    }
 | 
			
		||||
    a {
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
      color: inherit;
 | 
			
		||||
    }
 | 
			
		||||
    body > a  {
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      transform: translate(-50%, -50%);
 | 
			
		||||
      top: 50%;
 | 
			
		||||
      left: 50%;
 | 
			
		||||
      box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
 | 
			
		||||
      background: #fff;
 | 
			
		||||
      display: block;
 | 
			
		||||
      border-radius: 0.2em;
 | 
			
		||||
      width: 90%;
 | 
			
		||||
      max-width: 25em;
 | 
			
		||||
    }
 | 
			
		||||
    body > a > div:first-child {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      padding: 1em;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      background: #ffffff;
 | 
			
		||||
      color: rgba(0, 0, 0, 0.5);
 | 
			
		||||
      border-bottom: 1px solid rgba(0, 0, 0, 0.05);
 | 
			
		||||
    }
 | 
			
		||||
    body > a > div:last-child {
 | 
			
		||||
      padding: 2em 3em;
 | 
			
		||||
    }
 | 
			
		||||
    body > a * {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
    body > a h1 {
 | 
			
		||||
      margin-top: .2em;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <a href="?dl=1">
 | 
			
		||||
    <div>Download {{ if .File.IsDir }}Folder{{ else }}File{{ end }}</div>
 | 
			
		||||
    <div>
 | 
			
		||||
      {{ if .File.IsDir -}}
 | 
			
		||||
      <svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
        <path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
 | 
			
		||||
        <path d="M0 0h24v24H0z" fill="none"/>
 | 
			
		||||
      </svg>
 | 
			
		||||
      {{ else -}}
 | 
			
		||||
      <svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
        <path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
 | 
			
		||||
        <path d="M0 0h24v24H0z" fill="none"/>
 | 
			
		||||
      </svg>
 | 
			
		||||
      {{ end -}}
 | 
			
		||||
      <h1>{{ .File.Name }}</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
  </a>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +53,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	// If the format is true, just set it to "zip".
 | 
			
		||||
	if query == "true" {
 | 
			
		||||
	if query == "true" || query == "" {
 | 
			
		||||
		query = "zip"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,11 +62,13 @@ import (
 | 
			
		|||
	"reflect"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	rice "github.com/GeertJohan/go.rice"
 | 
			
		||||
	"github.com/asdine/storm"
 | 
			
		||||
	"github.com/hacdias/fileutils"
 | 
			
		||||
	"github.com/mholt/caddy"
 | 
			
		||||
	"github.com/robfig/cron"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
| 
						 | 
				
			
			@ -92,6 +94,9 @@ type FileManager struct {
 | 
			
		|||
	// The static assets.
 | 
			
		||||
	assets *rice.Box
 | 
			
		||||
 | 
			
		||||
	// Job cron.
 | 
			
		||||
	cron *cron.Cron
 | 
			
		||||
 | 
			
		||||
	// PrefixURL is a part of the URL that is already trimmed from the request URL before it
 | 
			
		||||
	// arrives to our handlers. It may be useful when using File Manager as a middleware
 | 
			
		||||
	// such as in caddy-filemanager plugin. It is only useful in certain situations.
 | 
			
		||||
| 
						 | 
				
			
			@ -205,6 +210,7 @@ func New(database string, base User) (*FileManager, error) {
 | 
			
		|||
	// map and Assets box.
 | 
			
		||||
	m := &FileManager{
 | 
			
		||||
		Users:  map[string]*User{},
 | 
			
		||||
		cron:   cron.New(),
 | 
			
		||||
		assets: rice.MustFindBox("./assets/dist"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -297,6 +303,10 @@ func New(database string, base User) (*FileManager, error) {
 | 
			
		|||
	base.Username = ""
 | 
			
		||||
	base.Password = ""
 | 
			
		||||
	m.DefaultUser = &base
 | 
			
		||||
 | 
			
		||||
	m.cron.AddFunc("@hourly", m.shareCleaner)
 | 
			
		||||
	m.cron.Start()
 | 
			
		||||
 | 
			
		||||
	return m, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -406,6 +416,29 @@ func (m *FileManager) enableJekyll(j *Jekyll) error {
 | 
			
		|||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// shareCleaner removes sharing links that are no longer active.
 | 
			
		||||
// This function is set to run periodically.
 | 
			
		||||
func (m FileManager) shareCleaner() {
 | 
			
		||||
	var links []shareLink
 | 
			
		||||
 | 
			
		||||
	// Get all links.
 | 
			
		||||
	err := m.db.All(&links)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Print(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Find the expired ones.
 | 
			
		||||
	for i := range links {
 | 
			
		||||
		if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
 | 
			
		||||
			err = m.db.DeleteStruct(&links[i])
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Print(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Allowed checks if the user has permission to access a directory/file.
 | 
			
		||||
func (u User) Allowed(url string) bool {
 | 
			
		||||
	var rule *Rule
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										81
									
								
								http.go
								
								
								
								
							
							
						
						
									
										81
									
								
								http.go
								
								
								
								
							| 
						 | 
				
			
			@ -6,6 +6,9 @@ import (
 | 
			
		|||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/asdine/storm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// RequestContext contains the needed information to make handlers work.
 | 
			
		||||
| 
						 | 
				
			
			@ -33,10 +36,9 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 | 
			
		|||
	// pass it through a template to add the needed variables.
 | 
			
		||||
	if r.URL.Path == "/sw.js" {
 | 
			
		||||
		return renderFile(
 | 
			
		||||
			w,
 | 
			
		||||
			c, w,
 | 
			
		||||
			c.assets.MustString("sw.js"),
 | 
			
		||||
			"application/javascript",
 | 
			
		||||
			c,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -65,16 +67,20 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 | 
			
		|||
		return c.StaticGen.Preview(c, w, r)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.HasPrefix(r.URL.Path, "/share/") && c.StaticGen != nil {
 | 
			
		||||
		r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
 | 
			
		||||
		return sharePage(c, w, r)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Any other request should show the index.html file.
 | 
			
		||||
	w.Header().Set("x-frame-options", "SAMEORIGIN")
 | 
			
		||||
	w.Header().Set("x-content-type", "nosniff")
 | 
			
		||||
	w.Header().Set("x-xss-protection", "1; mode=block")
 | 
			
		||||
 | 
			
		||||
	return renderFile(
 | 
			
		||||
		w,
 | 
			
		||||
		c, w,
 | 
			
		||||
		c.assets.MustString("index.html"),
 | 
			
		||||
		"text/html",
 | 
			
		||||
		c,
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -86,10 +92,9 @@ func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	return renderFile(
 | 
			
		||||
		w,
 | 
			
		||||
		c, w,
 | 
			
		||||
		c.assets.MustString("static/manifest.json"),
 | 
			
		||||
		"application/json",
 | 
			
		||||
		c,
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -154,6 +159,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
 | 
			
		|||
		code, err = usersHandler(c, w, r)
 | 
			
		||||
	case "settings":
 | 
			
		||||
		code, err = settingsHandler(c, w, r)
 | 
			
		||||
	case "share":
 | 
			
		||||
		code, err = shareHandler(c, w, r)
 | 
			
		||||
	default:
 | 
			
		||||
		code = http.StatusNotFound
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -194,7 +201,7 @@ func splitURL(path string) (string, string) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// renderFile renders a file using a template with some needed variables.
 | 
			
		||||
func renderFile(w http.ResponseWriter, file string, contentType string, c *RequestContext) (int, error) {
 | 
			
		||||
func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentType string) (int, error) {
 | 
			
		||||
	tpl := template.Must(template.New("file").Parse(file))
 | 
			
		||||
	w.Header().Set("Content-Type", contentType+"; charset=utf-8")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -209,6 +216,66 @@ func renderFile(w http.ResponseWriter, file string, contentType string, c *Reque
 | 
			
		|||
	return 0, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
			
		||||
	var s shareLink
 | 
			
		||||
	err := c.db.One("Hash", r.URL.Path, &s)
 | 
			
		||||
	if err == storm.ErrNotFound {
 | 
			
		||||
		return renderFile(
 | 
			
		||||
			c, w,
 | 
			
		||||
			c.assets.MustString("static/share/404.html"),
 | 
			
		||||
			"text/html",
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.Expires && s.ExpireDate.Before(time.Now()) {
 | 
			
		||||
		c.db.DeleteStruct(&s)
 | 
			
		||||
		return renderFile(
 | 
			
		||||
			c, w,
 | 
			
		||||
			c.assets.MustString("static/share/404.html"),
 | 
			
		||||
			"text/html",
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r.URL.Path = s.Path
 | 
			
		||||
 | 
			
		||||
	info, err := os.Stat(s.Path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorToHTTP(err, false), err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.File = &file{
 | 
			
		||||
		Path:    s.Path,
 | 
			
		||||
		Name:    info.Name(),
 | 
			
		||||
		ModTime: info.ModTime(),
 | 
			
		||||
		Mode:    info.Mode(),
 | 
			
		||||
		IsDir:   info.IsDir(),
 | 
			
		||||
		Size:    info.Size(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dl := r.URL.Query().Get("dl")
 | 
			
		||||
 | 
			
		||||
	if dl == "" || dl == "0" {
 | 
			
		||||
		tpl := template.Must(template.New("file").Parse(c.assets.MustString("static/share/index.html")))
 | 
			
		||||
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
 | 
			
		||||
 | 
			
		||||
		err := tpl.Execute(w, map[string]interface{}{
 | 
			
		||||
			"BaseURL": c.RootURL(),
 | 
			
		||||
			"File":    c.File,
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return http.StatusInternalServerError, err
 | 
			
		||||
		}
 | 
			
		||||
		return 0, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return downloadHandler(c, w, r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// renderJSON prints the JSON version of data to the browser.
 | 
			
		||||
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
 | 
			
		||||
	marsh, err := json.Marshal(data)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@
 | 
			
		|||
    "lint": "eslint --ext .js,.vue assets/src"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "clipboard": "^1.7.1",
 | 
			
		||||
    "codemirror": "^5.27.4",
 | 
			
		||||
    "filesize": "^3.5.10",
 | 
			
		||||
    "moment": "^2.18.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,6 @@ import (
 | 
			
		|||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/hacdias/fileutils"
 | 
			
		||||
	"github.com/robfig/cron"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// sanitizeURL sanitizes the URL to prevent path transversal
 | 
			
		||||
| 
						 | 
				
			
			@ -174,7 +173,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		// Otherwise we try to create the directory.
 | 
			
		||||
		err := c.User.FileSystem.Mkdir(r.URL.Path, 0666)
 | 
			
		||||
		err := c.User.FileSystem.Mkdir(r.URL.Path, 0776)
 | 
			
		||||
		return errorToHTTP(err, false), err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -188,7 +187,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	// Create/Open the file.
 | 
			
		||||
	f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
 | 
			
		||||
	f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorToHTTP(err, false), err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -242,15 +241,13 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
 | 
			
		|||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	scheduler := cron.New()
 | 
			
		||||
	scheduler.AddFunc(t.Format("05 04 15 02 01 *"), func() {
 | 
			
		||||
	c.cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
 | 
			
		||||
		_, err := resourcePublish(c, w, r)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Print(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	scheduler.Start()
 | 
			
		||||
	return http.StatusOK, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
d12113f698d3e44cb873181f6e7d7c9e17de30ac
 | 
			
		||||
61f496e0973436714c2e195318148550e39bbe26
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,137 @@
 | 
			
		|||
package filemanager
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/asdine/storm"
 | 
			
		||||
	"github.com/asdine/storm/q"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type shareLink struct {
 | 
			
		||||
	Hash       string    `json:"hash" storm:"id,index"`
 | 
			
		||||
	Path       string    `json:"path" storm:"index"`
 | 
			
		||||
	Expires    bool      `json:"expires"`
 | 
			
		||||
	ExpireDate time.Time `json:"expireDate"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
			
		||||
	r.URL.Path = sanitizeURL(r.URL.Path)
 | 
			
		||||
 | 
			
		||||
	switch r.Method {
 | 
			
		||||
	case http.MethodGet:
 | 
			
		||||
		return shareGetHandler(c, w, r)
 | 
			
		||||
	case http.MethodDelete:
 | 
			
		||||
		return shareDeleteHandler(c, w, r)
 | 
			
		||||
	case http.MethodPost:
 | 
			
		||||
		return sharePostHandler(c, w, r)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return http.StatusNotImplemented, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		s    []*shareLink
 | 
			
		||||
		path = filepath.Join(string(c.User.FileSystem), r.URL.Path)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	err := c.db.Find("Path", path, &s)
 | 
			
		||||
	if err == storm.ErrNotFound {
 | 
			
		||||
		return http.StatusNotFound, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, link := range s {
 | 
			
		||||
		if link.Expires && link.ExpireDate.Before(time.Now()) {
 | 
			
		||||
			c.db.DeleteStruct(&shareLink{Hash: link.Hash})
 | 
			
		||||
			s = append(s[:i], s[i+1:]...)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return renderJSON(w, s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
			
		||||
	path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
 | 
			
		||||
 | 
			
		||||
	var s shareLink
 | 
			
		||||
	expire := r.URL.Query().Get("expires")
 | 
			
		||||
	unit := r.URL.Query().Get("unit")
 | 
			
		||||
 | 
			
		||||
	if expire == "" {
 | 
			
		||||
		err := c.db.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&s)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
 | 
			
		||||
			return 0, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bytes, err := generateRandomBytes(32)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	str := hex.EncodeToString(bytes)
 | 
			
		||||
 | 
			
		||||
	s = shareLink{
 | 
			
		||||
		Path:    path,
 | 
			
		||||
		Hash:    str,
 | 
			
		||||
		Expires: expire != "",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if expire != "" {
 | 
			
		||||
		num, err := strconv.Atoi(expire)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return http.StatusInternalServerError, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var add time.Duration
 | 
			
		||||
		switch unit {
 | 
			
		||||
		case "seconds":
 | 
			
		||||
			add = time.Second * time.Duration(num)
 | 
			
		||||
		case "minutes":
 | 
			
		||||
			add = time.Minute * time.Duration(num)
 | 
			
		||||
		case "days":
 | 
			
		||||
			add = time.Hour * 24 * time.Duration(num)
 | 
			
		||||
		default:
 | 
			
		||||
			add = time.Hour * time.Duration(num)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		s.ExpireDate = time.Now().Add(add)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = c.db.Save(&s)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return renderJSON(w, s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
 | 
			
		||||
	var s shareLink
 | 
			
		||||
 | 
			
		||||
	err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)
 | 
			
		||||
	if err == storm.ErrNotFound {
 | 
			
		||||
		return http.StatusNotFound, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = c.db.DeleteStruct(&s)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return http.StatusOK, nil
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue