Skip to content

Commit

Permalink
Allow a person to tag buckets.
Browse files Browse the repository at this point in the history
This feature has a frontend and backend part. From the frontend, user can create tags at the create bucket screen. On the backend, the tags are stored as metadata in the swift object store.
  • Loading branch information
csc-felipe committed Dec 3, 2021
1 parent 8c9fda9 commit fa6bdea
Show file tree
Hide file tree
Showing 15 changed files with 360 additions and 49 deletions.
36 changes: 34 additions & 2 deletions swift_browser_ui/ui/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions swift_browser_ui/ui/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
46 changes: 44 additions & 2 deletions swift_browser_ui_frontend/src/common/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.");
}
Expand Down
15 changes: 15 additions & 0 deletions swift_browser_ui_frontend/src/common/conv.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@

import {
getBucketMeta,
} from "./api";

export default function getLangCookie () {
let matches = document.cookie.match(new RegExp(
"(?:^|; )" + "OBJ_UI_LANG" + "=([^;]*)",
Expand Down Expand Up @@ -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;
}
12 changes: 12 additions & 0 deletions swift_browser_ui_frontend/src/common/lang.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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?",
Expand All @@ -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",
Expand Down Expand Up @@ -286,6 +292,7 @@ let default_translations = {
created: "Luotu",
folderDetails: "Ei yksityiskohtia kansioille",
clearChecked: "Poista valinnat",
showTags: "Näytä tagit",
},
discover: {
sync_shares: "Synkronoi jaetut säiliöt",
Expand Down Expand Up @@ -409,18 +416,23 @@ let default_translations = {
copy: " Kopioi",
create: "Luo",
delete: "Poista",
edit: "Muokkaa",
save: "Tallena",
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: "Muokkaaminen säiliö: ",
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: "Tagit",
tagMessage: "Paina 'enter' lisätäksesi.",
},
objects: {
deleteConfirm: "Poista objektit",
Expand Down
5 changes: 5 additions & 0 deletions swift_browser_ui_frontend/src/common/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
57 changes: 44 additions & 13 deletions swift_browser_ui_frontend/src/common/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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"},
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default {
type: "is-success",
});
swiftDeleteContainer(this.container).then(() => {
this.$store.commit("updateContainers");
this.$store.dispatch("updateContainers");
});
},
},
Expand Down
2 changes: 1 addition & 1 deletion swift_browser_ui_frontend/src/entries/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
Loading

0 comments on commit fa6bdea

Please sign in to comment.