Skip to content

Commit

Permalink
Feature superset migration (#46)
Browse files Browse the repository at this point in the history
* can't edit asset name

* Update README.md

* Update README.md

* Update README.md

* search checks author

* version button exists

* version selection front-end done

* state included version

* bug fix, empty versions list

* i want more than 3 versions!!!!

* can no longer add assets with the same name

added error check for uploading of a new asset with the same name as one in the db. Also added user error display message

* Validation check added: asset name must be camelCase

* almost there, downloads versions, but idk what's downloaded

* support for new downloads json infrastructure

* file does not exist error fix

* VERSION DOWNLOAD DONE

* oopsies

* revert to main

* Remove ability to change asset name on backend

* Format, fix some typeerrors

* Refactor version download

- Update file to be named `store.json`, including user config (access key) for other DCC integrations
- Merge previous electron-store implementation with JSON store
- Tweak frontend version select UI a bit

* Add version selection by clicking on bubbles

---------

Co-authored-by: Kyra Clark <ckyra@seas.upenn.edu>
Co-authored-by: pojojojo21 <jofisch@seas.upenn.edu>
Co-authored-by: Thomas Shaw <printer.83mph@gmail.com>
  • Loading branch information
4 people authored Apr 27, 2024
1 parent c3d6432 commit 731ac83
Show file tree
Hide file tree
Showing 18 changed files with 261 additions and 140 deletions.
4 changes: 2 additions & 2 deletions backend/routers/api_v1/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
remove_asset,
)
from database.connection import get_db
from schemas.models import Asset, AssetCreate, Version, VersionCreate
from schemas.models import Asset, AssetCreate, AssetUpdate, Version, VersionCreate
from util.files import save_upload_file_temp
from util.auth import get_current_user, oauth2_scheme

Expand Down Expand Up @@ -103,7 +103,7 @@ async def put_asset(
db: Annotated[Session, Depends(get_db)],
token: Annotated[str, Depends(oauth2_scheme)],
uuid: str,
asset: AssetCreate,
asset: AssetUpdate,
):
result = update_asset(db, uuid, asset)
if result is None:
Expand Down
7 changes: 6 additions & 1 deletion backend/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@


class AssetBase(BaseModel):
asset_name: str
keywords: str
image_uri: Optional[str]


class AssetCreate(AssetBase):
asset_name: str
pass


class AssetUpdate(AssetBase):
pass


class Asset(AssetBase):
asset_name: str
id: UUID
author_pennkey: str

Expand Down
9 changes: 5 additions & 4 deletions backend/util/crud/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from schemas.models import (
AssetCreate,
AssetUpdate,
VersionCreate,
Asset as MAsset,
Version as MVersion,
Expand Down Expand Up @@ -54,11 +55,12 @@ def read_assets(
*[Asset.asset_name.ilike(f"%{kw}%") for kw in keywords],
)
)
# check if keywords contain search words
# check if keywords or author contain search words
query = query.filter(
or_(
*asset_name_conditions,
*[Asset.keywords.ilike(f"%{kw}%") for kw in keywords],
*[Asset.author_pennkey.ilike(f"%{search}%")],
)
)

Expand Down Expand Up @@ -99,15 +101,14 @@ def create_asset(db: Session, asset: AssetCreate, author_pennkey: str):
return db_asset


def update_asset(db: Session, asset_id: str, asset: AssetCreate):
def update_asset(db: Session, asset_id: str, asset: AssetUpdate):
db_asset = (
db.execute(select(Asset).filter(Asset.id == asset_id).limit(1))
.scalars()
.first()
)
if db_asset is None:
return None
db_asset.asset_name = asset.asset_name
db_asset.keywords = asset.keywords
db_asset.image_uri = asset.image_uri
db.commit()
Expand Down Expand Up @@ -141,7 +142,7 @@ def read_asset_info(db: Session, asset_id: str):
.outerjoin(Version, Version.asset_id == Asset.id)
.filter(Asset.id == asset_id)
.order_by(Version.date.desc())
.limit(3)
.limit(10)
)
results = db.execute(query).mappings().all()

Expand Down
111 changes: 63 additions & 48 deletions frontend/src/main/lib/local-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { existsSync } from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';

import { DownloadedEntry } from '../../types/ipc';
import fetchClient from './fetch-client';
import store from './store';
import { DownloadedEntry, Version } from '../../types/ipc';
import { getAuthToken } from './authentication';
import fetchClient from './fetch-client';
import store, { griddleFrontendStore } from './store';

// TODO: clean up error handling here + in message-handlers

export function getDownloadFolder() {
const downloadPath = store.get('downloadFolder') as string;
const downloadPath = griddleFrontendStore.get('storeLocation');

// Ensure the download folder exists
if (!existsSync(downloadPath)) {
Expand All @@ -26,8 +28,26 @@ export function setDownloadFolder(downloadFolder: string) {
store.set('downloadFolder', downloadFolder);
}

export function getStoredVersions() {
return store.get('versions', []);
export function getDownloadedVersions() {
return store.get('downloadedAssetVersions', []);
}

export function getDownloadedVersionByID(asset_id: string) {
return getDownloadedVersions().find(({ asset_id: id }) => asset_id === id);
}

function setDownloadedVersion(
asset_id: string,
{ semver, folderName }: Omit<DownloadedEntry, 'asset_id'>,
) {
const downloads = store.get('downloadedAssetVersions');

const newDownloads = [
...downloads.filter(({ asset_id: id }) => id !== asset_id),
{ asset_id, semver, folderName },
] satisfies DownloadedEntry[];

store.set('downloadedAssetVersions', newDownloads);
}

/**
Expand All @@ -48,12 +68,11 @@ export async function createInitialVersion({
await fsPromises.mkdir(folderPath, { recursive: true });

console.log('adding to store');
const newEntry = { asset_id, semver: null, folderName } satisfies DownloadedEntry;
store.set('versions', [...getStoredVersions(), newEntry]);
setDownloadedVersion(asset_id, { semver: null, folderName });
}

export async function openFolder(asset_id: string, semver: string | null) {
const stored = getStoredVersions().find((v) => v.asset_id === asset_id && v.semver === semver);
export async function openFolder(asset_id: string) {
const stored = getDownloadedVersionByID(asset_id);
if (!stored) return;

shell.openPath(path.join(getDownloadFolder(), stored.folderName));
Expand Down Expand Up @@ -88,25 +107,22 @@ async function zipFolder(sourceFolder: string, zipFilePath: string): Promise<voi
});
}

export async function commitChanges(
asset_id: string,
semver: string | null,
message: string,
is_major: boolean,
) {
const folderName = getStoredVersions().find(
(v) => v.asset_id === asset_id && v.semver === semver,
)?.folderName;

/**
* Given an asset name, creates and uploads a new "commit", updating
* remote and local store to match
*
* @returns Downloaded asset list, updated
*/
export async function commitChanges(asset_id: string, message: string, is_major: boolean) {
const folderName = getDownloadedVersionByID(asset_id)?.folderName;
console.log('folderName', folderName);

if (!folderName) {
console.log('no folder name found for', asset_id, semver);
return;
throw new Error(`no folder name found for asset with id ${asset_id}`);
}

const sourceFolder = path.join(getDownloadFolder(), folderName);
const zipFilePath = path.join(app.getPath('temp'), `${asset_id}_${semver}.zip`);
const zipFilePath = path.join(app.getPath('temp'), `${asset_id}_commit.zip`);
console.log('sourceFolder is: ', sourceFolder); // debug log
console.log('zipFilePath is: ', zipFilePath);

Expand All @@ -116,7 +132,7 @@ export async function commitChanges(
const fileData = new Blob([fileContents], { type: 'application/zip' });

// Uploading Zip file with multipart/form-data
const { response, error } = await fetchClient.POST('/api/v1/assets/{uuid}/versions', {
const result = await fetchClient.POST('/api/v1/assets/{uuid}/versions', {
params: { path: { uuid: asset_id } },
body: {
file: fileData as unknown as string,
Expand All @@ -126,31 +142,35 @@ export async function commitChanges(
headers: { Authorization: `Bearer ${getAuthToken()}` },
bodySerializer(body) {
const formData = new FormData();
formData.append('file', body.file as unknown as Blob, `${asset_id}_${semver}.zip`);
formData.append('file', body.file as unknown as Blob, `${asset_id}_commit.zip`);
formData.append('message', body.message);
formData.append('is_major', (body.is_major ?? false).toString());
return formData;
},
});

const { error, response } = result;

if (error || !response.ok) {
console.log('error uploading zip file', error, response.status);
throw new Error(`Failed to upload zip file for asset ${asset_id}`);
}

// TODO: make this remove old version entry from store
// Update local store with the new version
const newVersion = { asset_id, semver, folderName };
const versions = getStoredVersions();
versions.push(newVersion);
store.set('versions', versions);
// Update store with currently downloaded version
const { semver } = result.data as Version;
setDownloadedVersion(asset_id, { semver, folderName });

// Clean up the zip file
await fsPromises.rm(zipFilePath);

return store.get('versions', []);
return getDownloadedVersions();
}

/**
* Downloads a specified version of an asset, logging it to the local store.
*
* If the asset is already downloaded, it will be overwritten.
*/
export async function downloadVersion({ asset_id, semver }: { asset_id: string; semver: string }) {
console.log('fetching metadata...');
let asset_name;
Expand Down Expand Up @@ -188,40 +208,35 @@ export async function downloadVersion({ asset_id, semver }: { asset_id: string;
// previously had semver in here but probably not necessary
// const folderName = `${asset_name}_${semver}_${asset_id.substring(0, 8)}/`;
const folderName = `${asset_name}_${asset_id.substring(0, 8)}/`;
// remove old copy of folder
await fsPromises.rm(path.join(getDownloadFolder(), folderName), { force: true, recursive: true });
await extract(zipFilePath, { dir: path.join(getDownloadFolder(), folderName) });

console.log('removing zip file...');
await fsPromises.rm(zipFilePath);

console.log('marking as done!');
store.set('versions', [
...getStoredVersions(),
{ asset_id, semver, folderName } satisfies DownloadedEntry,
]);
console.log('marking as stored!');
setDownloadedVersion(asset_id, { semver, folderName });

console.log('we made it! check', getDownloadFolder());
return getDownloadedVersions();
}

/**
* Removes a version from the store and deletes the associated folder
*/
export async function removeVersion({
asset_id,
semver,
}: {
asset_id: string;
semver: string | null;
}) {
const versions = getStoredVersions();
export async function unsyncAsset(asset_id: string) {
const versions = getDownloadedVersions();

const stored = versions.find((v) => v.asset_id === asset_id && v.semver === semver);
const stored = versions.find((v) => v.asset_id === asset_id);
if (!stored) return;

// delete folder
const folderPath = path.join(getDownloadFolder(), stored.folderName);
await fsPromises.rm(folderPath, { recursive: true });

// remove from store
const newVersions = versions.filter((v) => v.asset_id !== asset_id || v.semver !== semver);
store.set('versions', newVersions);
const newVersions = versions.filter((v) => v.asset_id !== asset_id);

store.set('downloadedAssetVersions', newVersions);
}
26 changes: 21 additions & 5 deletions frontend/src/main/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,34 @@ import { DownloadedEntry } from '../../types/ipc';
import { app } from 'electron';
import path from 'node:path';

interface GriddleFrontendStoreSchema {
storeLocation: string;
}

const griddleFrontendDefaults: GriddleFrontendStoreSchema = {
storeLocation: path.join(app.getPath('documents'), 'Griddle'),
};

// This store is only for this frontend, to control where the main store is
export const griddleFrontendStore = new Store<GriddleFrontendStoreSchema>({
defaults: griddleFrontendDefaults,
});

interface StoreSchema {
versions: DownloadedEntry[];
downloadFolder: string;
downloadedAssetVersions: DownloadedEntry[];
authToken: string | null;
}

const defaults: StoreSchema = {
versions: [],
downloadFolder: path.join(app.getPath('documents'), 'Griddle'),
downloadedAssetVersions: [],
authToken: null,
};

const store = new Store<StoreSchema>({ defaults });
// This store keeps track of which asset/version pairings are downloaded
const store = new Store<StoreSchema>({
defaults,
cwd: griddleFrontendStore.get('storeLocation'),
name: 'store',
});

export default store;
22 changes: 11 additions & 11 deletions frontend/src/main/message-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import {
commitChanges,
createInitialVersion,
downloadVersion,
getStoredVersions,
getDownloadedVersions,
openFolder,
removeVersion,
unsyncAsset,
} from './lib/local-assets';

// Types for these can be found in `src/types/ipc.d.ts`
const messageHandlers: MessageHandlers = {
'assets:list-downloaded': async () => {
// console.log('getting downloaded:', getStoredVersions());
return { ok: true, versions: getStoredVersions() };
return { ok: true, versions: getDownloadedVersions() };
},
'assets:download-asset': async (_, { asset_id }) => {
// TODO
Expand All @@ -29,18 +29,18 @@ const messageHandlers: MessageHandlers = {
await downloadVersion({ asset_id, semver });
return { ok: true };
},
'assets:remove-version': async (_, { asset_id, semver }) => {
await removeVersion({ asset_id, semver });
'assets:remove-download': async (_, { asset_id }) => {
await unsyncAsset(asset_id);
return { ok: true };
},
'assets:commit-changes': async (_, { asset_id, semver, message, is_major }) => {
console.log(`Committing changes for ${asset_id}@${semver}`);
await commitChanges(asset_id, semver, message, is_major);
'assets:commit-changes': async (_, { asset_id, message, is_major }) => {
console.log(`Committing changes for ${asset_id}`);
await commitChanges(asset_id, message, is_major);
return { ok: true };
},
'assets:open-folder': async (_, { asset_id, semver }) => {
console.log(`Opening folder for ${asset_id}@${semver}`);
await openFolder(asset_id, semver);
'assets:open-folder': async (_, { asset_id }) => {
console.log(`Opening folder for ${asset_id}`);
await openFolder(asset_id);
return { ok: true };
},
'auth:get-auth-token': async () => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/renderer/src/components/asset-entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function AssetEntry({

const onDownloadClick = async () => {
setDownloading(true);
await syncAsset({ uuid: id });
await syncAsset({ uuid: id, asset_name });
setDownloading(false);
await mutateDownloads();
};
Expand All @@ -34,7 +34,7 @@ export default function AssetEntry({
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
setSelected(id);
setSelected(id, null);
}}
className={`group inline-flex h-full w-full flex-col rounded-2xl bg-base-100 p-3 text-left shadow transition-shadow focus-visible:outline-none ${isSelected ? 'ring-2 ring-primary/60 focus-visible:outline-none focus-visible:ring-4' : 'ring-primary/40 focus-visible:ring-4'}`}
>
Expand Down
Loading

0 comments on commit 731ac83

Please sign in to comment.