Skip to content

Commit

Permalink
feat: upload thumbnail pngs back (#223)
Browse files Browse the repository at this point in the history
* feat: better upload for thumbnails

* feat: lock png upload with auth

* fix: if token for auth not present, also error out

* feat: upload protein png on upload

* feat: delete goes back to search

* feat: on component destroy, release webgl memory
  • Loading branch information
xnought authored Apr 6, 2024
1 parent f608262 commit 8b94d70
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 106 deletions.
11 changes: 6 additions & 5 deletions backend/src/api/protein.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from fastapi.exceptions import HTTPException

from ..api_types import ProteinEntry, UploadBody, UploadError, EditBody, CamelModel
from ..auth import requiresAuthentication
from ..auth import requires_authentication
from ..tmalign import tm_align
from io import BytesIO
from fastapi import APIRouter
Expand Down Expand Up @@ -193,7 +193,7 @@ def get_protein_entry(protein_name: str):
# TODO: add permissions so only the creator can delete not just anyone
@router.delete("/protein/entry/{protein_name:str}", response_model=None)
def delete_protein_entry(protein_name: str, req: Request):
requiresAuthentication(req)
requires_authentication(req)
# Todo, have a meaningful error if the delete fails
with Database() as db:
# remove protein
Expand All @@ -210,7 +210,8 @@ def delete_protein_entry(protein_name: str, req: Request):


@router.post("/protein/upload/png", response_model=None)
def upload_protein_png(body: UploadPNGBody):
def upload_protein_png(body: UploadPNGBody, req: Request):
requires_authentication(req)
with Database() as db:
try:
query = """UPDATE proteins SET thumbnail = %s WHERE name = %s"""
Expand All @@ -222,7 +223,7 @@ def upload_protein_png(body: UploadPNGBody):
# None return means success
@router.post("/protein/upload", response_model=UploadError | None)
def upload_protein_entry(body: UploadBody, req: Request):
requiresAuthentication(req)
requires_authentication(req)

body.name = format_protein_name(body.name)
# check that the name is not already taken in the DB
Expand Down Expand Up @@ -290,7 +291,7 @@ def edit_protein_entry(body: EditBody, req: Request):

# check that the name is not already taken in the DB
# TODO: check if permission so we don't have people overriding other people's names
requiresAuthentication(req)
requires_authentication(req)
try:
# replace spaces in the name with underscores
body.old_name = format_protein_name(body.old_name)
Expand Down
4 changes: 2 additions & 2 deletions backend/src/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from passlib.hash import bcrypt
from ..api_types import LoginBody, LoginResponse
from ..db import Database
from ..auth import generateAuthToken
from ..auth import generate_auth_token

router = APIRouter()

Expand Down Expand Up @@ -32,7 +32,7 @@ def login(body: LoginBody):
return LoginResponse(token="", error="Invalid Email or Password")

# Generates the token and returns
token = generateAuthToken(email, admin)
token = generate_auth_token(email, admin)
log.warn("Giving token:", token)
return LoginResponse(token=token, error="")

Expand Down
15 changes: 10 additions & 5 deletions backend/src/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
secret_key = "SuperSecret"


def generateAuthToken(userId, admin):
def generate_auth_token(userId, admin):
payload = {
"email": userId,
"admin": admin,
Expand All @@ -18,13 +18,13 @@ def generateAuthToken(userId, admin):
return jwt.encode(payload, secret_key, algorithm="HS256")


def authenticateToken(token):
def authenticate_token(token):
# Return the decoded token if it's valid.
try:
# Valid token is always is in the form "Bearer [token]", so we need to slice off the "Bearer" portion.
sliced_token = token[7:]
log.warn(sliced_token)
decoded = jwt.decode(sliced_token, secret_key, algorithms="HS256")
decoded = jwt.decode(sliced_token, secret_key, algorithms=["HS256"])
log.warn("Valid token")
log.warn(decoded)
return decoded
Expand All @@ -36,8 +36,13 @@ def authenticateToken(token):


# Use this function with a request if you want.
def requiresAuthentication(req: Request):
userInfo = authenticateToken(req.headers["authorization"])
def requires_authentication(req: Request):
# no header at all
if "authorization" not in req.headers:
raise HTTPException(status_code=403, detail="Unauthorized")

# verify token is good if provided
userInfo = authenticate_token(req.headers["authorization"])
if not userInfo or not userInfo.get("admin"):
log.error("Unauthorized User")
raise HTTPException(status_code=403, detail="Unauthorized")
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/ListProteins.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
>
<div class="prot-thumb mr-2">
<img
loading="lazy"
class="prot-thumb"
src={entry.thumbnail ?? ""}
alt="thumbnail"
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/lib/Molstar.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
<script lang="ts">
import { onDestroy } from "svelte";
import { PDBeMolstarPlugin } from "../../venome-molstar/lib";
import { loseWebGLContext } from "./venomeMolstarUtils";
export let url = "";
export let format = "pdb";
export let bgColor = { r: 255, g: 255, b: 255 }; // white
export let binary = false;
export let width = 500;
export let height = 500;
let m: PDBeMolstarPlugin;
let divEl: HTMLDivElement;
async function render() {
// @ts-ignore
const m = new PDBeMolstarPlugin(); // loaded through app.html
m = new PDBeMolstarPlugin();
// some bs for the whole thing to rerender. TODO: fix this.
divEl.innerHTML = "";
const div = document.createElement("div");
Expand All @@ -33,6 +35,11 @@
});
}
onDestroy(() => {
loseWebGLContext(divEl.querySelector("canvas")!);
m.plugin.dispose();
});
$: {
if (url && divEl) {
render();
Expand Down
76 changes: 76 additions & 0 deletions frontend/src/lib/venomeMolstarUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { BACKEND_URL } from "./backend";
import { PDBeMolstarPlugin } from "../../venome-molstar/lib";
import type { InitParams } from "../../venome-molstar/lib/spec";

export async function screenshotMolstar(initParams: Partial<InitParams>) {
const { div, molstar } = await renderHeadless(initParams);
const imgData = await getPreview(molstar);

// cleanup
loseWebGLContext(div.querySelector("canvas")!);
molstar.plugin.dispose();
div.remove();

return imgData;
}

export function loseWebGLContext(canvas: HTMLCanvasElement) {
const gl = canvas.getContext("webgl");
if (gl) {
const loseContext = gl.getExtension("WEBGL_lose_context");
if (loseContext) {
loseContext.loseContext();
}
}
}

async function getPreview(m: PDBeMolstarPlugin, checkDelayMs = 25) {
const blankPreview =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAFACAYAAADNkKWqAAAAAXNSR0IArs4c6QAACUJJREFUeF7t1AENADAMAsHNv2iWzMZfHXA03G07jgABAkGBawCDrYtMgMAXMIAegQCBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AgAH0AwQIZAUMYLZ6wQkQMIB+gACBrIABzFYvOAECBtAPECCQFTCA2eoFJ0DAAPoBAgSyAgYwW73gBAgYQD9AgEBWwABmqxecAAED6AcIEMgKGMBs9YITIGAA/QABAlkBA5itXnACBAygHyBAICtgALPVC06AwAM7Tfx9MOLD/gAAAABJRU5ErkJggg==";
while (true) {
const imgData = m.plugin.helpers.viewportScreenshot
?.getPreview()!
.canvas.toDataURL()!;
if (imgData !== blankPreview) {
return imgData;
}
await delay(checkDelayMs);
}
}

async function renderHeadless(initParams: Partial<InitParams>) {
const molstar = new PDBeMolstarPlugin();
const div = document.createElement("div");
await molstar.render(div, initParams);
return { div, molstar };
}

async function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export const defaultInitParams = (name: string): Partial<InitParams> => ({
customData: {
url: `${BACKEND_URL}/protein/pdb/${name}`,
format: "pdb",
binary: false,
},
subscribeEvents: false,
bgColor: {
r: 255,
g: 255,
b: 255,
},
selectInteraction: false,
alphafoldView: true,
reactive: false,
sequencePanel: false,
hideControls: true,
hideCanvasControls: [
"animation",
"expand",
"selection",
"controlToggle",
"controlInfo",
],
});
2 changes: 1 addition & 1 deletion frontend/src/routes/Edit.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
on:click={async () => {
setToken();
await Backend.deleteProteinEntry(urlId);
navigate("/");
navigate("/search");
}}>Delete Protein Entry</Button
>
</div>
Expand Down
102 changes: 15 additions & 87 deletions frontend/src/routes/ForceUploadThumbnails.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<script lang="ts">
import { onMount } from "svelte";
import { Backend, type ProteinEntry } from "../lib/backend";
import { PDBeMolstarPlugin } from "../../venome-molstar/lib";
let divEl: HTMLDivElement;
let m: PDBeMolstarPlugin;
let mounted = false;
import { Backend, type ProteinEntry, setToken } from "../lib/backend";
import {
screenshotMolstar,
defaultInitParams,
} from "../lib/venomeMolstarUtils";
let proteinsNeedingPng: ProteinEntry[] = [];
let uploaded = 0;
let preview = "";
Expand All @@ -14,99 +13,28 @@
proteinsNeedingPng = allProteins.proteinEntries.filter((protein) => {
return protein.thumbnail === null;
});
// proteinsNeedingPng = allProteins.proteinEntries;
mounted = true;
});
$: if (mounted) {
getImageDataForAllProteins(proteinsNeedingPng);
}
async function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function base64ToBytes(base64: string) {
const binString = atob(base64);
console.log(binString);
//@ts-ignore
return Uint32Array.from(binString, (m) => m.codePointAt(0));
}
async function screenshot(delayMs = 200) {
async function onLoad(funcToExec: () => void) {
return new Promise((resolve) => {
m.events.loadComplete.subscribe(() => {
funcToExec();
resolve(true);
});
});
}
async function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
let p = "";
await onLoad(() => {
console.log("protein loaded");
});
await delay(delayMs); // why the fuck do I need this for the next line to work?
p = m.plugin.helpers.viewportScreenshot
?.getPreview()!
.canvas.toDataURL()!;
return p;
}
async function getImageDataForAllProteins(proteins: ProteinEntry[]) {
for (let i = 0; i < proteins.length; i++) {
const protein = proteins[i];
// remove the canvas within the div
m = new PDBeMolstarPlugin();
await m.render(divEl, {
customData: {
url: `http://localhost:8000/protein/pdb/${protein.name}`,
format: "pdb",
binary: false,
},
subscribeEvents: false,
bgColor: {
r: 255,
g: 255,
b: 255,
},
selectInteraction: false,
alphafoldView: true,
reactive: false,
sequencePanel: false,
hideControls: true,
hideCanvasControls: [
"animation",
"expand",
"selection",
"controlToggle",
"controlInfo",
],
});
const b64 = await screenshot();
let i = 0;
for (const protein of proteinsNeedingPng) {
const b64 = await screenshotMolstar(
defaultInitParams(protein.name)
);
preview = b64;
setToken();
await Backend.uploadProteinPng({
base64Encoding: b64,
proteinName: protein.name,
});
await m.clear();
uploaded++;
}
}
});
</script>

<div
bind:this={divEl}
style="width: 400px; height: 350px; float: left; position: relative;"
></div>

<div>
Uploading {uploaded} of {proteinsNeedingPng.length}
</div>
<div>
preview
{proteinsNeedingPng[uploaded]?.name}
</div>
<div>
<img src={preview} />
</div>
21 changes: 17 additions & 4 deletions frontend/src/routes/Upload.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<script lang="ts">
import {
screenshotMolstar,
defaultInitParams,
} from "../lib/venomeMolstarUtils";
import { Backend, UploadError, setToken } from "../lib/backend";
import {
Fileupload,
Expand Down Expand Up @@ -108,7 +112,7 @@

const pdbFileStr = await fileToString(file);
try {
setToken()
setToken();
const err = await Backend.uploadProteinEntry({
name,
description,
Expand All @@ -121,9 +125,18 @@
uploadError = err;
console.log(uploadError);
} else {
// success, so we can go back!
// TODO: make the name processing only in the backend and we just send back in the err object above
navigate(`/protein/${formatProteinName(name)}`);
// success, we can also upload the png thumbnail
const dbProteinNameFormat = formatProteinName(name);
const b64 = await screenshotMolstar(
defaultInitParams(dbProteinNameFormat)
);
await Backend.uploadProteinPng({
base64Encoding: b64,
proteinName: dbProteinNameFormat,
});

// then go to its new protein page
navigate(`/protein/${dbProteinNameFormat}`);
}
} catch (e) {
console.log(e);
Expand Down

0 comments on commit 8b94d70

Please sign in to comment.