From ebb53e715b8af8a83517317da87fe85ff32e97a9 Mon Sep 17 00:00:00 2001 From: Laurynas Gadliauskas Date: Fri, 11 Jun 2021 14:43:00 +0300 Subject: [PATCH] feat: user quota display (#12) --- cmd/users.go | 3 ++ frontend/src/api/users.js | 6 +++ frontend/src/components/Quota.vue | 71 +++++++++++++++++++++++++++++ frontend/src/components/Sidebar.vue | 6 +++ frontend/src/css/base.css | 25 ++++++++++ frontend/src/i18n/en.json | 6 ++- frontend/src/store/index.js | 3 +- frontend/src/store/modules/quota.js | 32 +++++++++++++ frontend/src/utils/buttons.js | 2 +- frontend/src/utils/constants.js | 2 + http/http.go | 2 + http/quota.go | 42 +++++++++++++++++ http/static.go | 1 + settings/defaults.go | 2 + users/users.go | 1 + 15 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/Quota.vue create mode 100644 frontend/src/store/modules/quota.js create mode 100644 http/quota.go diff --git a/cmd/users.go b/cmd/users.go index df535c97..f14e3708 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -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": diff --git a/frontend/src/api/users.js b/frontend/src/api/users.js index 7975d66a..1f12c503 100644 --- a/frontend/src/api/users.js +++ b/frontend/src/api/users.js @@ -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", + }); +} diff --git a/frontend/src/components/Quota.vue b/frontend/src/components/Quota.vue new file mode 100644 index 00000000..647d1f07 --- /dev/null +++ b/frontend/src/components/Quota.vue @@ -0,0 +1,71 @@ + + + diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 5e607a9d..40c1eda6 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -45,6 +45,8 @@ + +
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, diff --git a/frontend/src/css/base.css b/frontend/src/css/base.css index 970d81f7..a3b4bbc0 100644 --- a/frontend/src/css/base.css +++ b/frontend/src/css/base.css @@ -143,4 +143,29 @@ 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; } \ No newline at end of file diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 30d210e7..53c1524f 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -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!" diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index fb8c0cb6..6c9e5dc8 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -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 }, }); diff --git a/frontend/src/store/modules/quota.js b/frontend/src/store/modules/quota.js new file mode 100644 index 00000000..28e66fc1 --- /dev/null +++ b/frontend/src/store/modules/quota.js @@ -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 }; diff --git a/frontend/src/utils/buttons.js b/frontend/src/utils/buttons.js index 7b897662..45706209 100644 --- a/frontend/src/utils/buttons.js +++ b/frontend/src/utils/buttons.js @@ -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; diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 666c3adf..d30f8b90 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -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, diff --git a/http/http.go b/http/http.go index bc798561..1a6332c2 100644 --- a/http/http.go +++ b/http/http.go @@ -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") diff --git a/http/quota.go b/http/quota.go new file mode 100644 index 00000000..2155cda7 --- /dev/null +++ b/http/quota.go @@ -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) +}) diff --git a/http/static.go b/http/static.go index d3c3765f..88313ae5 100644 --- a/http/static.go +++ b/http/static.go @@ -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, diff --git a/settings/defaults.go b/settings/defaults.go index 8f0109e3..ba1b3c94 100644 --- a/settings/defaults.go +++ b/settings/defaults.go @@ -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 diff --git a/users/users.go b/users/users.go index 9279f0cf..ff543928 100644 --- a/users/users.go +++ b/users/users.go @@ -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"`