feat: user quota display (#12)

pull/3756/head
Laurynas Gadliauskas 2021-06-11 14:43:00 +03:00 committed by GitHub
parent 53df153a70
commit ebb53e715b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 201 additions and 3 deletions

View File

@ -75,6 +75,7 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.StringSlice("commands", nil, "a list of the commands a user can execute")
flags.String("scope", ".", "scope for users")
flags.String("trashDir", "", "trash directory path for users")
flags.String("quotaFile", "", "path to file with quota data")
flags.String("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
flags.Bool("singleClick", false, "use single clicks only")
@ -96,6 +97,8 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
defaults.Scope = mustGetString(flags, flag.Name)
case "trashDir":
defaults.TrashDir = mustGetString(flags, flag.Name)
case "quotaFile":
defaults.QuotaFile = mustGetString(flags, flag.Name)
case "locale":
defaults.Locale = mustGetString(flags, flag.Name)
case "viewMode":

View File

@ -49,3 +49,9 @@ export async function remove(id) {
throw new Error(res.status);
}
}
export async function getQuota() {
return await fetchJSON(`/api/quota`, {
method: "GET",
});
}

View File

@ -0,0 +1,71 @@
<template>
<div id="quota">
<div>
<label>{{ $t("sidebar.quota.space") }}</label>
<div class="bar" :title="spaceUsageTitle">
<div class="progress" :style="{ width: spaceProgress + '%' }"></div>
</div>
</div>
<div>
<label>{{ $t("sidebar.quota.inodes") }}</label>
<div class="bar" :title="inodeUsageTitle">
<div class="progress" :style="{ width: inodeProgress + '%' }"></div>
</div>
</div>
</div>
</template>
<script>
import filesize from "filesize";
import { mapState } from "vuex";
export default {
name: "quota",
computed: {
...mapState("quota", {
inodes: (state) => state.inodes,
space: (state) => state.space,
}),
loaded() {
return this.inodes !== null && this.space !== null;
},
spaceProgress() {
if (!this.loaded) {
return 0;
}
return this.progress(this.space);
},
inodeProgress() {
if (!this.loaded) {
return 0;
}
return this.progress(this.inodes);
},
spaceUsageTitle() {
if (this.space === null) {
return "- / -";
} else {
return filesize(this.space.usage) + " / " + filesize(this.space.quota);
}
},
inodeUsageTitle() {
if (this.inodes === null) {
return "- / -";
} else {
return this.inodes.usage + " / " + this.inodes.quota;
}
},
},
mounted() {
this.$store.dispatch("quota/fetch");
},
methods: {
progress(metric) {
let prc = (metric.usage / metric.quota) * 100;
return Math.min(prc, 100);
},
},
};
</script>

View File

@ -45,6 +45,8 @@
</router-link>
</div>
<quota v-if="quotaExists"></quota>
<div>
<router-link
class="action"
@ -117,11 +119,13 @@
<script>
import { mapState, mapGetters } from "vuex";
import * as auth from "@/utils/auth";
import Quota from "@/components/Quota";
import {
version,
signup,
disableExternal,
trashDir,
quotaExists,
noAuth,
authMethod,
authLogoutURL,
@ -129,6 +133,7 @@ import {
export default {
name: "sidebar",
components: { Quota },
computed: {
...mapState(["user"]),
...mapGetters(["isLogged"]),
@ -139,6 +144,7 @@ export default {
version: () => version,
disableExternal: () => disableExternal,
trashDir: () => trashDir,
quotaExists: () => quotaExists,
noAuth: () => noAuth,
authMethod: () => authMethod,
authLogoutURL: () => authLogoutURL,

View File

@ -144,3 +144,28 @@ main {
.break-word {
word-break: break-all;
}
#quota {
padding: 1em;
}
#quota > div + div {
margin-top: .5em;
}
#quota label {
color: #546E7A;
}
#quota .bar {
width: 100%;
height: 10px;
background-color: rgba(0,0,0, 0.1);
}
#quota .bar .progress {
width: 0;
height: 100%;
transition: .2s ease width;
background-color: #40c4ff;
}

View File

@ -267,7 +267,11 @@
"preview": "Preview",
"settings": "Settings",
"signup": "Signup",
"siteSettings": "Site Settings"
"siteSettings": "Site Settings",
"quota": {
"space": "Space",
"inodes": "Inodes"
}
},
"success": {
"linkCopied": "Link copied!"

View File

@ -3,6 +3,7 @@ import Vuex from "vuex";
import mutations from "./mutations";
import getters from "./getters";
import upload from "./modules/upload";
import quota from "./modules/quota";
Vue.use(Vuex);
@ -31,5 +32,5 @@ export default new Vuex.Store({
state,
getters,
mutations,
modules: { upload },
modules: { upload, quota },
});

View File

@ -0,0 +1,32 @@
import { users as api } from "@/api";
const state = {
inodes: null,
space: null,
};
const mutations = {
setQuota(state, { inodes, space }) {
state.inodes = inodes;
state.space = space;
},
};
const actions = {
fetch: async (context) => {
try {
let data = await api.getQuota();
if (
data !== null &&
data.inodes != undefined &&
data.space != undefined
) {
context.commit("setQuota", data);
}
} catch (e) {
console.log(e);
}
},
};
export default { state, mutations, actions, namespaced: true };

View File

@ -48,7 +48,7 @@ function success(button) {
el.style.opacity = 0;
setTimeout(() => {
el.classList.remove("spin");
el.classList.remove("pulse-spin");
el.innerHTML = "done";
el.style.opacity = 1;

View File

@ -8,6 +8,7 @@ const signup = window.FileBrowser.Signup;
const version = window.FileBrowser.Version;
const logoURL = `${staticURL}/img/logo.svg`;
const trashDir = window.FileBrowser.TrashDir;
const quotaExists = window.FileBrowser.QuotaExists;
const noAuth = window.FileBrowser.NoAuth;
const authMethod = window.FileBrowser.AuthMethod;
const authLogoutURL = window.FileBrowser.AuthLogoutURL;
@ -27,6 +28,7 @@ export {
signup,
version,
trashDir,
quotaExists,
noAuth,
authMethod,
authLogoutURL,

View File

@ -67,6 +67,8 @@ func NewHandler(
api.Handle("/settings", monkey(settingsGetHandler, "")).Methods("GET")
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
api.Handle("/quota", monkey(quotaGetHandler, "")).Methods("GET")
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
api.PathPrefix("/preview/{size}/{path:.*}").
Handler(monkey(previewHandler(imgSvc, fileCache, server.EnableThumbnails, server.ResizePreview), "/api/preview")).Methods("GET")

42
http/quota.go Normal file
View File

@ -0,0 +1,42 @@
package http
import (
"encoding/json"
"net/http"
"os"
)
type quotaData struct {
InodeLimit uint64 `json:"inodeLimit"`
InodeQuota uint64 `json:"inodeQuota"`
InodeUsage uint64 `json:"inodeUsage"`
SpaceLimit uint64 `json:"spaceLimit"`
SpaceQuota uint64 `json:"spaceQuota"`
SpaceUsage uint64 `json:"spaceUsage"`
}
var quotaGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
content, err := os.ReadFile(d.user.QuotaFile)
if err != nil {
return errToStatus(err), err
}
data := quotaData{}
err = json.Unmarshal(content, &data)
if err != nil {
return errToStatus(err), err
}
res := map[string]map[string]uint64{
"inodes": {
"quota": data.InodeQuota,
"usage": data.InodeUsage,
},
"space": {
"quota": data.SpaceQuota,
"usage": data.SpaceUsage,
},
}
return renderJSON(w, r, res)
})

View File

@ -34,6 +34,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
"StaticURL": path.Join(d.server.BaseURL, "/static"),
"Signup": d.settings.Signup,
"TrashDir": d.settings.Defaults.TrashDir,
"QuotaExists": d.settings.Defaults.QuotaFile != "",
"NoAuth": d.settings.AuthMethod == auth.MethodNoAuth,
"AuthMethod": d.settings.AuthMethod,
"AuthLogoutURL": d.settings.AuthLogoutURL,

View File

@ -10,6 +10,7 @@ import (
type UserDefaults struct {
Scope string `json:"scope"`
TrashDir string `json:"trashDir"`
QuotaFile string `json:"quotaFile"`
Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"`
@ -23,6 +24,7 @@ type UserDefaults struct {
func (d *UserDefaults) Apply(u *users.User) {
u.Scope = d.Scope
u.TrashDir = d.TrashDir
u.QuotaFile = d.QuotaFile
u.Locale = d.Locale
u.ViewMode = d.ViewMode
u.SingleClick = d.SingleClick

View File

@ -26,6 +26,7 @@ type User struct {
Password string `json:"password"`
Scope string `json:"scope"`
TrashDir string `json:"trashDir"`
QuotaFile string `json:"quotaFile"`
Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"`
ViewMode ViewMode `json:"viewMode"`