diff --git a/docs/_static/api.yml b/docs/_static/api.yml index e45e00246..c906c3baa 100644 --- a/docs/_static/api.yml +++ b/docs/_static/api.yml @@ -747,6 +747,37 @@ paths: description: Unauthorized 404: description: Not Found + post: + tags: + - Frontend + summary: Update user created container metadata. + parameters: + - name: container + in: query + description: The container which metadata will be updated. + schema: + type: string + example: test-container-1 + required: true + requestBody: + description: Bucket metadata as a key-value object. Updates must include all the metadata for the bucket. Omitted keys are removed by the swift backend. + required: true + content: + application/json: + schema: + type: object + properties: + key: + type: string + example: + owner: project-team + responses: + 204: + description: Container metadata was updated. No Content. + 403: + description: Unauthorized + 404: + description: Container was not found /api/shared/objects: get: tags: diff --git a/swift_browser_ui/ui/api.py b/swift_browser_ui/ui/api.py index 0da1405b0..363d0647f 100644 --- a/swift_browser_ui/ui/api.py +++ b/swift_browser_ui/ui/api.py @@ -84,11 +84,20 @@ async def swift_create_container(request: aiohttp.web.Request) -> aiohttp.web.Re request.app["Log"].info( f"API call for container creation from {request.remote}, sess {session}" ) + + req_json = await request.json() + tags = req_json.get("tags", None) + + headers = {} + if tags: + headers["X-Container-Meta-UserTags"] = tags + # Shamelessly use private methods from SwiftService to avoid writing # own implementation res = request.app["Sessions"][session]["ST_conn"]._create_container_job( - get_conn(request.app["Sessions"][session]["ST_conn"]._options), - request.match_info["container"], + conn=get_conn(request.app["Sessions"][session]["ST_conn"]._options), + container=request.match_info["container"], + headers=headers, ) except (SwiftError, ClientException): request.app["Log"].error("Container creation failed.") @@ -527,6 +536,29 @@ async def get_metadata_bucket(request: aiohttp.web.Request) -> aiohttp.web.Respo return aiohttp.web.json_response([ret["container"], ret["headers"]]) +async def update_metadata_bucket(request: aiohttp.web.Request) -> aiohttp.web.Response: + """Update metadata for a container.""" + session = api_check(request) + request.app["Log"].info( + "API cal for updating container metadata from " + f"{request.remote}, sess: {session} :: {time.ctime()}" + ) + + # Get required variables from query string + container = request.query.get("container", "") or None + meta = await request.json() + + meta = [(key, value) for key, value in meta.items()] + + conn = request.app["Sessions"][session]["ST_conn"] + ret = conn.post(container=container, options={"meta": meta}) + + if not ret["success"]: + raise aiohttp.web.HTTPNotFound + + return aiohttp.web.HTTPNoContent() + + async def get_metadata_object(request: aiohttp.web.Request) -> aiohttp.web.Response: """Get metadata for a container or for an object.""" session = api_check(request) diff --git a/swift_browser_ui/ui/server.py b/swift_browser_ui/ui/server.py index 8610a9f0c..5bbfab4a8 100644 --- a/swift_browser_ui/ui/server.py +++ b/swift_browser_ui/ui/server.py @@ -44,6 +44,7 @@ swift_upload_object_chunk, swift_check_object_chunk, swift_replicate_container, + update_metadata_bucket, ) from swift_browser_ui.ui.health import handle_health_check from swift_browser_ui.ui.settings import setd @@ -158,6 +159,7 @@ async def servinit() -> aiohttp.web.Application: aiohttp.web.get("/api/projects", os_list_projects), aiohttp.web.get("/api/project/active", get_os_active_project), aiohttp.web.get("/api/bucket/meta", get_metadata_bucket), + aiohttp.web.post("/api/bucket/meta", update_metadata_bucket), aiohttp.web.get("/api/bucket/object/meta", get_metadata_object), aiohttp.web.get("/api/project/meta", get_project_metadata), aiohttp.web.get("/api/project/acl", get_access_control_metadata), diff --git a/swift_browser_ui_frontend/src/common/api.js b/swift_browser_ui_frontend/src/common/api.js index 884123005..06b701212 100644 --- a/swift_browser_ui_frontend/src/common/api.js +++ b/swift_browser_ui_frontend/src/common/api.js @@ -72,6 +72,40 @@ export async function getBuckets () { return buckets; } +export async function getBucketMeta ( + container, +){ + let url = new URL( + "/api/bucket/meta?container=".concat(encodeURI(container)), + document.location.origin, + ); + + let ret = await fetch( + url, {method: "GET", credentials: "same-origin"}, + ); + return ret.json(); +} + +export async function updateBucketMeta ( + container, + metadata, +){ + let url = new URL( + "/api/bucket/meta?container=".concat(encodeURI(container)), + document.location.origin, + ); + + let ret = await fetch( + url, + { + method: "POST", + credentials: "same-origin", + body: JSON.stringify(metadata), + }, + ); + return ret; +} + export async function getObjects (container) { // Fetch objects contained in a container from the API for the user // that's currently logged in. @@ -227,21 +261,29 @@ export async function getSharedContainerAddress () { export async function swiftCreateContainer ( container, + tags, ) { // Create a container matching the specified name. let fetchURL = new URL( "/api/containers/".concat( container, ), document.location.origin); + let body = { + tags, + }; let ret = await fetch( - fetchURL, { method: "PUT", credentials: "same-origin" }, + fetchURL, { + method: "PUT", + credentials: "same-origin", + body: JSON.stringify(body), + }, ); if (ret.status != 201) { if (ret.status == 409) { throw new Error("Container name already in use."); } if (ret.status == 400) { - throw new Error("Invalid container name"); + throw new Error("Invalid container or tag name"); } throw new Error("Container creation not successful."); } diff --git a/swift_browser_ui_frontend/src/common/conv.js b/swift_browser_ui_frontend/src/common/conv.js index b5246c5c9..eb6794467 100644 --- a/swift_browser_ui_frontend/src/common/conv.js +++ b/swift_browser_ui_frontend/src/common/conv.js @@ -1,3 +1,8 @@ + +import { + getBucketMeta, +} from "./api"; + export default function getLangCookie () { let matches = document.cookie.match(new RegExp( "(?:^|; )" + "OBJ_UI_LANG" + "=([^;]*)", @@ -51,3 +56,13 @@ export function getHumanReadableSize (val) { } return ret; } + +export async function getTagsForContainer(containerName) { + let tags = []; + await getBucketMeta(containerName).then(meta => { + if ("usertags" in meta[1]) { + tags = meta[1]["usertags"].split(";"); + } + }); + return tags; +} diff --git a/swift_browser_ui_frontend/src/common/lang.js b/swift_browser_ui_frontend/src/common/lang.js index 716341a6a..531b994fd 100644 --- a/swift_browser_ui_frontend/src/common/lang.js +++ b/swift_browser_ui_frontend/src/common/lang.js @@ -63,6 +63,7 @@ let default_translations = { created: "Created", folderDetails: "No details for folders", clearChecked: "Clear checked", + showTags: "Display tags", }, discover: { sync_shares: "Synchronize shared buckets", @@ -185,12 +186,15 @@ let default_translations = { copy: " Copy", create: "Create", delete: "Delete", + edit: "Edit", + save: "Save", createContainerButton: "Create Bucket", copysuccess: "Started copying the bucket in the background", copyfail: "Failed to copy the bucket", renderFolders: "Render as Folders", container_ops: { addContainer: "Add a new bucket", + editContainer: "Editing bucket: ", deleteConfirm: "Delete Bucket", deleteConfirmMessage: "Are you sure you want to delete this " + "bucket?", @@ -199,6 +203,8 @@ let default_translations = { containerMessage: "The name of the new bucket", fullDelete: "Deleting a bucket with contents requires deleting " + "all objects inside it first.", + tagName: "Tags", + tagMessage: "Press enter to add.", }, objects: { deleteConfirm: "Delete Objects", @@ -286,6 +292,7 @@ let default_translations = { created: "Luotu", folderDetails: "Ei yksityiskohtia kansioille", clearChecked: "Poista valinnat", + showTags: "Näytä Tägit", }, discover: { sync_shares: "Synkronoi jaetut säiliöt", @@ -409,18 +416,23 @@ let default_translations = { copy: " Kopioi", create: "Luo", delete: "Poista", + edit: "Muokkaa", + save: "Tallenna", createContainerButton: "Luo säiliö", copysuccess: "Aloitettiin säiliön kopiointi taustalla", copyfail: "Säiliön kopiointi epäonnistui", renderFolders: "Näytä kansioina", container_ops: { addContainer: "Luo uusi säiliö", + editContainer: "Muokataan säiliötä: ", deleteConfirm: "Poista säiliö", deleteConfirmMessage: "Haluatko varmasti poistaa tämän säiliön?", deleteSuccess: "Säiliö poistettu", containerName: "Säiliö", containerMessage: "Uuden säiliön nimi", fullDelete: "Säiliön sisältö on poistettava ennen säiliön postamista.", + tagName: "Tägit", + tagMessage: "Paina 'enter' lisätäksesi.", }, objects: { deleteConfirm: "Poista objektit", diff --git a/swift_browser_ui_frontend/src/common/router.js b/swift_browser_ui_frontend/src/common/router.js index 60339c98f..6329b1dcd 100644 --- a/swift_browser_ui_frontend/src/common/router.js +++ b/swift_browser_ui_frontend/src/common/router.js @@ -44,6 +44,11 @@ export default new Router({ name: "AddContainer", component: CreateContainer, }, + { + path: "/browse/:user/:project/:container/edit", + name: "EditContainer", + component: CreateContainer, + }, { path: "/browse/:user/:project/sharing/requestdirect", name: "DirectRequest", diff --git a/swift_browser_ui_frontend/src/common/store.js b/swift_browser_ui_frontend/src/common/store.js index 697c95a62..5239d7f19 100644 --- a/swift_browser_ui_frontend/src/common/store.js +++ b/swift_browser_ui_frontend/src/common/store.js @@ -3,10 +3,13 @@ import Vue from "vue"; import Vuex from "vuex"; import { getBuckets } from "@/common/api"; -import { +import { getObjects, getSharedObjects, } from "./api"; +import { + getTagsForContainer, +} from "./conv"; Vue.use(Vuex); @@ -20,6 +23,7 @@ const store = new Vuex.Store({ isFullPage: true, objectCache: [], containerCache: [], + containerTagsCache: {}, // {"containerName": ["tag1", "tag2"]} langs: [ {ph: "In English", value: "en"}, {ph: "Suomeksi", value: "fi"}, @@ -33,18 +37,12 @@ const store = new Vuex.Store({ altContainer: undefined, }, mutations: { - updateContainers (state) { + loading(state, payload) { + state.isLoading = payload; + }, + updateContainers (state, payload) { // Update container cache with the new container listing. - state.isLoading = true; - getBuckets().then((ret) => { - if (ret.status != 200) { - state.isLoading = false; - } - state.containerCache = ret; - state.isLoading = false; - }).catch(() => { - state.isLoading = false; - }); + state.containerCache = payload; }, updateObjects ( state, @@ -89,7 +87,13 @@ const store = new Vuex.Store({ }); } }, - eraseObjects (state) { + updateContainerTags(state, payload) { + state.containerTagsCache = { + ...state.containerTagsCache, + [payload.containerName]: payload.tags, + }; + }, + eraseObjects(state) { state.objectCache = []; }, setProjects (state, newProjects) { @@ -146,6 +150,33 @@ const store = new Vuex.Store({ state.altContainer = undefined; }, }, + actions: { + updateContainers: async function ({ commit, dispatch }) { + commit("loading", true); + let containers = []; + await getBuckets().then((ret) => { + if (ret.status != 200) { + commit("loading", false); + } + containers = ret; + commit("updateContainers", ret); + commit("loading", false); + }).catch(() => { + commit("loading", false); + }); + dispatch("updateContainerTags", containers); + return containers; + }, + updateContainerTags: function ({ commit }, containers) { + containers.map(async container => { + const tags = await getTagsForContainer(container.name); + commit( + "updateContainerTags", + {containerName: container.name, tags}, + ); + }); + }, + }, }); export default store; diff --git a/swift_browser_ui_frontend/src/components/ContainerDeleteButton.vue b/swift_browser_ui_frontend/src/components/ContainerDeleteButton.vue index 125970ce3..e395a8f17 100644 --- a/swift_browser_ui_frontend/src/components/ContainerDeleteButton.vue +++ b/swift_browser_ui_frontend/src/components/ContainerDeleteButton.vue @@ -55,7 +55,7 @@ export default { type: "is-success", }); swiftDeleteContainer(this.container).then(() => { - this.$store.commit("updateContainers"); + this.$store.dispatch("updateContainers"); }); }, }, diff --git a/swift_browser_ui_frontend/src/entries/main.js b/swift_browser_ui_frontend/src/entries/main.js index 64e708dbd..622bef363 100644 --- a/swift_browser_ui_frontend/src/entries/main.js +++ b/swift_browser_ui_frontend/src/entries/main.js @@ -205,7 +205,7 @@ new Vue({ endUpload: function () { this.$store.commit("eraseAltContainer"); this.$store.commit("stopUploading"); - this.$store.commit("updateContainers"); + this.$store.dispatch("updateContainers"); window.onbeforeunload = undefined; }, startChunking: function () { diff --git a/swift_browser_ui_frontend/src/views/AddContainer.vue b/swift_browser_ui_frontend/src/views/AddContainer.vue index f57f32c4f..e54996195 100644 --- a/swift_browser_ui_frontend/src/views/AddContainer.vue +++ b/swift_browser_ui_frontend/src/views/AddContainer.vue @@ -4,7 +4,11 @@ class="contents" >
+