Compare commits
9 Commits
main
...
feat/table
Author | SHA1 | Date |
---|---|---|
Tomáš Mládek | 4a2305722c | |
Tomáš Mládek | cb6460032d | |
Tomáš Mládek | 90c9725469 | |
Tomáš Mládek | 24457a5b6a | |
Tomáš Mládek | dc2cf94ba7 | |
Tomáš Mládek | 1483497479 | |
Tomáš Mládek | 3e51645510 | |
Tomáš Mládek | 3e42fbd60a | |
Tomáš Mládek | 8dda83fbd7 |
|
@ -1,6 +1,6 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="dev backend" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="command" value="run -- serve ./example_vault --clean --no-browser --reinitialize --rescan-mode mirror --secret upend" />
|
||||
<option name="command" value="run -- serve ./example_vault --clean --no-browser --reinitialize --rescan-mode mirror" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
|
|
@ -6,7 +6,7 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION ]
|
||||
secrets: [EARTHLY_CONFIGURATION]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
|
@ -19,7 +19,7 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION ]
|
||||
secrets: [EARTHLY_CONFIGURATION]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
|
@ -52,14 +52,13 @@ pipeline:
|
|||
SSH_CONFIG,
|
||||
SSH_UPLOAD_KEY,
|
||||
SSH_KNOWN_HOSTS,
|
||||
SENTRY_AUTH_TOKEN
|
||||
]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
- earthly --secret GPG_SIGN_KEY --secret SSH_CONFIG --secret SSH_UPLOAD_KEY --secret SSH_KNOWN_HOSTS +deploy-appimage-nightly
|
||||
when:
|
||||
branch: [ main ]
|
||||
branch: [main]
|
||||
|
||||
docker:nightly:
|
||||
image: earthly/earthly:v0.8.3
|
||||
|
@ -68,7 +67,7 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION, DOCKER_USER, DOCKER_PASSWORD, SENTRY_AUTH_TOKEN ]
|
||||
secrets: [EARTHLY_CONFIGURATION, DOCKER_USER, DOCKER_PASSWORD]
|
||||
commands:
|
||||
- echo $${DOCKER_PASSWORD}| docker login --username $${DOCKER_USER} --password-stdin
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
|
@ -76,7 +75,7 @@ pipeline:
|
|||
- earthly --push +docker-minimal
|
||||
- earthly --push +docker
|
||||
when:
|
||||
branch: [ main ]
|
||||
branch: [main]
|
||||
|
||||
docker:release:
|
||||
image: earthly/earthly:v0.8.3
|
||||
|
@ -85,7 +84,7 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION, DOCKER_USER, DOCKER_PASSWORD, SENTRY_AUTH_TOKEN ]
|
||||
secrets: [EARTHLY_CONFIGURATION, DOCKER_USER, DOCKER_PASSWORD]
|
||||
commands:
|
||||
- echo $${DOCKER_PASSWORD}| docker login --username $${DOCKER_USER} --password-stdin
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
|
@ -95,7 +94,7 @@ pipeline:
|
|||
- earthly --strict --push +docker --tag=latest
|
||||
- earthly --strict --push +docker --tag=$CI_COMMIT_TAG
|
||||
when:
|
||||
event: [ tag ]
|
||||
event: [tag]
|
||||
|
||||
jslib:publish:
|
||||
image: earthly/earthly:v0.8.3
|
||||
|
@ -104,13 +103,13 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION, NPM_TOKEN ]
|
||||
secrets: [EARTHLY_CONFIGURATION, NPM_TOKEN]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
- earthly --strict --push --secret NPM_TOKEN +publish-js-all
|
||||
when:
|
||||
branch: [ main ]
|
||||
branch: [main]
|
||||
|
||||
gitea:prerelease:
|
||||
image: earthly/earthly:v0.8.3
|
||||
|
@ -127,7 +126,7 @@ pipeline:
|
|||
- rm -rf dist
|
||||
when:
|
||||
event: [ tag ]
|
||||
|
||||
|
||||
appimage:release:
|
||||
image: earthly/earthly:v0.8.3
|
||||
volumes:
|
||||
|
@ -135,15 +134,15 @@ pipeline:
|
|||
environment:
|
||||
- FORCE_COLOR=1
|
||||
- EARTHLY_EXEC_CMD="/bin/sh"
|
||||
secrets: [ EARTHLY_CONFIGURATION, REGISTRY, REGISTRY_USER, REGISTRY_PASSWORD, SENTRY_AUTH_TOKEN ]
|
||||
secrets: [ EARTHLY_CONFIGURATION, REGISTRY, REGISTRY_USER, REGISTRY_PASSWORD ]
|
||||
commands:
|
||||
- mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
|
||||
- earthly bootstrap
|
||||
- mkdir -p dist/
|
||||
- earthly --strict -a '+appimage-signed/*' dist/
|
||||
when:
|
||||
event: [ tag ]
|
||||
|
||||
event: [tag]
|
||||
|
||||
# todo: webext
|
||||
|
||||
gitea:release:
|
||||
|
@ -158,4 +157,4 @@ pipeline:
|
|||
target: main
|
||||
note: CHANGELOG_CURRENT.md
|
||||
when:
|
||||
event: [ tag ]
|
||||
event: [tag]
|
|
@ -487,18 +487,6 @@ version = "1.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.7"
|
||||
|
@ -567,12 +555,6 @@ version = "0.21.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
@ -585,15 +567,6 @@ version = "2.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "1.0.1"
|
||||
|
@ -890,9 +863,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.12"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
||||
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -1137,7 +1110,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1773,9 +1745,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
version = "0.2.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
|
@ -2282,17 +2254,6 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae"
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
|
@ -2953,12 +2914,6 @@ version = "0.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
|
@ -3424,7 +3379,6 @@ name = "upend-db"
|
|||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
"chrono",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
|
@ -3440,7 +3394,6 @@ dependencies = [
|
|||
"nonempty",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"password-hash",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
|
|
28
Earthfile
28
Earthfile
|
@ -37,7 +37,7 @@ base-node:
|
|||
|
||||
base-frontend:
|
||||
FROM +base-node
|
||||
COPY +jslib/dist sdks/js/dist
|
||||
COPY +jslib/jslib sdks/js
|
||||
WORKDIR webui
|
||||
RUN rm -rf node_modules && pnpm install --frozen-lockfile
|
||||
|
||||
|
@ -65,14 +65,14 @@ wasmlib:
|
|||
wasm-pack build --target nodejs --out-dir pkg-node
|
||||
RUN sed -e 's%"name": "upend_wasm"%"name": "@upnd/wasm-web"%' -i pkg-web/package.json && \
|
||||
sed -e 's%"name": "upend_wasm"%"name": "@upnd/wasm-node"%' -i pkg-node/package.json
|
||||
SAVE ARTIFACT pkg-web
|
||||
SAVE ARTIFACT pkg-node
|
||||
SAVE ARTIFACT pkg-web AS LOCAL wasm/pkg-web
|
||||
SAVE ARTIFACT pkg-node AS LOCAL wasm/pkg-node
|
||||
|
||||
jslib:
|
||||
FROM +base-node
|
||||
WORKDIR sdks/js
|
||||
RUN pnpm build
|
||||
SAVE ARTIFACT dist
|
||||
SAVE ARTIFACT . jslib
|
||||
|
||||
webext:
|
||||
FROM +base-node
|
||||
|
@ -228,7 +228,7 @@ publish-js-wasm:
|
|||
base-npm-publish:
|
||||
FROM +base-node
|
||||
RUN --secret NPM_TOKEN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > $HOME/.npmrc
|
||||
COPY +jslib/dist sdks/js/dist
|
||||
COPY +jslib/jslib sdks/js
|
||||
|
||||
NPM_PUBLISH:
|
||||
FUNCTION
|
||||
|
@ -269,20 +269,4 @@ update-changelog:
|
|||
LOCALLY
|
||||
COPY +changelog/CHANGELOG.md .
|
||||
RUN git add CHANGELOG.md && git commit -m "release: Update CHANGELOG"
|
||||
RUN --push git push
|
||||
|
||||
dev-local:
|
||||
FROM debian:bookworm
|
||||
COPY +jslib/dist /js-dist
|
||||
COPY +wasmlib/pkg-web /wasm-web
|
||||
COPY +wasmlib/pkg-node /wasm-node
|
||||
SAVE ARTIFACT /js-dist AS LOCAL sdks/js/dist
|
||||
SAVE ARTIFACT /wasm-web AS LOCAL wasm/pkg-web
|
||||
SAVE ARTIFACT /wasm-node AS LOCAL wasm/pkg-node
|
||||
|
||||
dev-update-sdk:
|
||||
LOCALLY
|
||||
WORKDIR sdks/js
|
||||
RUN pnpm build
|
||||
WORKDIR webui
|
||||
RUN pnpm install
|
||||
RUN --push git push
|
|
@ -103,14 +103,12 @@ impl Address {
|
|||
UP_UUID => Ok(Address::Uuid(
|
||||
Uuid::from_slice(digest.as_slice()).map_err(UpEndError::from_any)?,
|
||||
)),
|
||||
UP_ATTRIBUTE => {
|
||||
let attribute = String::from_utf8(digest).map_err(UpEndError::from_any)?;
|
||||
if attribute.is_empty() {
|
||||
Ok(Address::Attribute(Attribute::null()))
|
||||
} else {
|
||||
Ok(Address::Attribute(attribute.parse()?))
|
||||
}
|
||||
}
|
||||
UP_ATTRIBUTE => Ok(Address::Attribute(
|
||||
String::from_utf8(digest)
|
||||
.map_err(UpEndError::from_any)?
|
||||
.as_str()
|
||||
.parse()?,
|
||||
)),
|
||||
UP_URL => Ok(Address::Url(
|
||||
Url::parse(&String::from_utf8(digest).map_err(UpEndError::from_any)?)
|
||||
.map_err(UpEndError::from_any)?,
|
||||
|
@ -257,9 +255,6 @@ mod tests {
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::addressing::{Address, IDENTITY};
|
||||
use crate::constants::{
|
||||
TYPE_ATTRIBUTE_ADDRESS, TYPE_HASH_ADDRESS, TYPE_URL_ADDRESS, TYPE_UUID_ADDRESS,
|
||||
};
|
||||
use crate::hash::{LargeMultihash, UpMultihash};
|
||||
|
||||
use super::UpEndError;
|
||||
|
@ -272,11 +267,6 @@ mod tests {
|
|||
let encoded = addr.encode()?;
|
||||
let decoded = Address::decode(&encoded)?;
|
||||
assert_eq!(addr, decoded);
|
||||
|
||||
let addr = &*TYPE_HASH_ADDRESS;
|
||||
let encoded = addr.encode()?;
|
||||
let decoded = Address::decode(&encoded)?;
|
||||
assert_eq!(addr, &decoded);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -286,11 +276,6 @@ mod tests {
|
|||
let encoded = addr.encode()?;
|
||||
let decoded = Address::decode(&encoded)?;
|
||||
assert_eq!(addr, decoded);
|
||||
|
||||
let addr = &*TYPE_UUID_ADDRESS;
|
||||
let encoded = addr.encode()?;
|
||||
let decoded = Address::decode(&encoded)?;
|
||||
assert_eq!(addr, &decoded);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -300,11 +285,6 @@ mod tests {
|
|||
let encoded = addr.encode()?;
|
||||
let decoded = Address::decode(&encoded)?;
|
||||
assert_eq!(addr, decoded);
|
||||
|
||||
let addr = &*TYPE_ATTRIBUTE_ADDRESS;
|
||||
let encoded = addr.encode()?;
|
||||
let decoded = Address::decode(&encoded)?;
|
||||
assert_eq!(addr, &decoded);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -314,11 +294,6 @@ mod tests {
|
|||
let encoded = addr.encode()?;
|
||||
let decoded = Address::decode(&encoded)?;
|
||||
assert_eq!(addr, decoded);
|
||||
|
||||
let addr = &*TYPE_URL_ADDRESS;
|
||||
let encoded = addr.encode()?;
|
||||
let decoded = Address::decode(&encoded)?;
|
||||
assert_eq!(addr, &decoded);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,6 @@ pub struct Entry {
|
|||
pub attribute: Attribute,
|
||||
pub value: EntryValue,
|
||||
pub provenance: String,
|
||||
pub user: Option<String>,
|
||||
pub timestamp: NaiveDateTime,
|
||||
}
|
||||
|
||||
|
@ -62,7 +61,6 @@ pub struct InvariantEntry {
|
|||
pub value: EntryValue,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum EntryValue {
|
||||
|
@ -82,7 +80,6 @@ impl TryFrom<&InvariantEntry> for Entry {
|
|||
attribute: invariant.attribute.clone(),
|
||||
value: invariant.value.clone(),
|
||||
provenance: "INVARIANT".to_string(),
|
||||
user: None,
|
||||
timestamp: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -79,7 +79,6 @@ impl TryFrom<lexpr::Value> for Attribute {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum QueryPart {
|
||||
Matches(PatternQuery),
|
||||
|
|
|
@ -4,4 +4,5 @@ pub struct UpEndConfig {
|
|||
pub desktop_enabled: bool,
|
||||
pub trust_executables: bool,
|
||||
pub secret: String,
|
||||
pub key: Option<String>,
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use upend_db::stores::Blob;
|
|||
use upend_db::{
|
||||
jobs::{JobContainer, JobState},
|
||||
stores::{fs::FILE_MIME_KEY, UpStore},
|
||||
BlobMode, OperationContext, UpEndConnection,
|
||||
BlobMode, UpEndConnection,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
|
@ -26,7 +26,6 @@ lazy_static! {
|
|||
attribute: ATTR_LABEL.parse().unwrap(),
|
||||
value: "ID3".into(),
|
||||
provenance: "INVARIANT".to_string(),
|
||||
user: None,
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
};
|
||||
}
|
||||
|
@ -40,7 +39,6 @@ impl Extractor for ID3Extractor {
|
|||
connection: &UpEndConnection,
|
||||
store: Arc<Box<dyn UpStore + Send + Sync>>,
|
||||
mut job_container: JobContainer,
|
||||
context: OperationContext,
|
||||
) -> Result<Vec<Entry>> {
|
||||
if let Address::Hash(hash) = address {
|
||||
let files = store.retrieve(hash)?;
|
||||
|
@ -74,16 +72,14 @@ impl Extractor for ID3Extractor {
|
|||
"TYER" | "TBPM" => EntryValue::guess_from(text),
|
||||
_ => text.clone().into(),
|
||||
},
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
},
|
||||
Entry {
|
||||
entity: Address::Attribute(format!("ID3_{}", frame.id()).parse()?),
|
||||
attribute: ATTR_LABEL.parse().unwrap(),
|
||||
value: format!("ID3: {}", frame.name()).into(),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
},
|
||||
]);
|
||||
|
@ -101,14 +97,12 @@ impl Extractor for ID3Extractor {
|
|||
Blob::from_filepath(&tmp_path),
|
||||
None,
|
||||
Some(BlobMode::StoreOnly),
|
||||
context.clone(),
|
||||
)?;
|
||||
result.push(Entry {
|
||||
entity: address.clone(),
|
||||
attribute: "ID3_PICTURE".parse()?,
|
||||
value: EntryValue::Address(Address::Hash(hash)),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
});
|
||||
has_pictures = true;
|
||||
|
@ -118,8 +112,7 @@ impl Extractor for ID3Extractor {
|
|||
entity: Address::Attribute("ID3_PICTURE".parse()?),
|
||||
attribute: ATTR_LABEL.parse().unwrap(),
|
||||
value: "ID3 Embedded Image".into(),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
})
|
||||
}
|
||||
|
@ -133,8 +126,7 @@ impl Extractor for ID3Extractor {
|
|||
entity: Address::Attribute(e.attribute.clone()),
|
||||
attribute: ATTR_OF.parse().unwrap(),
|
||||
value: EntryValue::Address(ID3_TYPE_INVARIANT.entity().unwrap()),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
})
|
||||
.collect::<Vec<Entry>>(),
|
||||
|
@ -146,8 +138,7 @@ impl Extractor for ID3Extractor {
|
|||
entity: address.clone(),
|
||||
attribute: ATTR_IN.parse().unwrap(),
|
||||
value: EntryValue::Address(ID3_TYPE_INVARIANT.entity().unwrap()),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -12,7 +12,7 @@ use upend_base::{
|
|||
use upend_db::{
|
||||
jobs::{JobContainer, JobState},
|
||||
stores::{fs::FILE_MIME_KEY, UpStore},
|
||||
OperationContext, UpEndConnection,
|
||||
UpEndConnection,
|
||||
};
|
||||
|
||||
pub struct ExifExtractor;
|
||||
|
@ -31,7 +31,6 @@ lazy_static! {
|
|||
value: "EXIF".into(),
|
||||
provenance: "INVARIANT".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: None
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -42,7 +41,6 @@ impl Extractor for ExifExtractor {
|
|||
_connection: &UpEndConnection,
|
||||
store: Arc<Box<dyn UpStore + Send + Sync>>,
|
||||
mut job_container: JobContainer,
|
||||
context: OperationContext,
|
||||
) -> Result<Vec<Entry>> {
|
||||
if let Address::Hash(hash) = address {
|
||||
let files = store.retrieve(hash)?;
|
||||
|
@ -88,16 +86,14 @@ impl Extractor for ExifExtractor {
|
|||
EntryValue::guess_from(format!("{}", field.display_value()))
|
||||
}
|
||||
},
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
},
|
||||
Entry {
|
||||
entity: Address::Attribute(attribute),
|
||||
attribute: ATTR_LABEL.parse().unwrap(),
|
||||
value: format!("EXIF: {}", tag_description).into(),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
},
|
||||
]);
|
||||
|
@ -113,8 +109,7 @@ impl Extractor for ExifExtractor {
|
|||
entity: Address::Attribute(e.attribute.clone()),
|
||||
attribute: ATTR_OF.parse().unwrap(),
|
||||
value: EntryValue::Address(EXIF_TYPE_INVARIANT.entity().unwrap()),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
})
|
||||
.collect::<Vec<Entry>>(),
|
||||
|
@ -128,8 +123,7 @@ impl Extractor for ExifExtractor {
|
|||
entity: address.clone(),
|
||||
attribute: ATTR_IN.parse().unwrap(),
|
||||
value: EntryValue::Address(EXIF_TYPE_INVARIANT.entity().unwrap()),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -3,7 +3,6 @@ use std::{process::Command, sync::Arc};
|
|||
use super::Extractor;
|
||||
use anyhow::{anyhow, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use tracing::{debug, trace};
|
||||
use upend_base::{
|
||||
addressing::Address,
|
||||
constants::{ATTR_IN, ATTR_KEY, ATTR_LABEL, ATTR_OF},
|
||||
|
@ -12,7 +11,7 @@ use upend_base::{
|
|||
use upend_db::{
|
||||
jobs::{JobContainer, JobState},
|
||||
stores::{fs::FILE_MIME_KEY, UpStore},
|
||||
OperationContext, UpEndConnection,
|
||||
UpEndConnection,
|
||||
};
|
||||
|
||||
const DURATION_KEY: &str = "MEDIA_DURATION";
|
||||
|
@ -28,7 +27,6 @@ lazy_static! {
|
|||
value: "Multimedia".into(),
|
||||
provenance: "INVARIANT".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: None,
|
||||
};
|
||||
pub static ref DURATION_OF_MEDIA: Entry = Entry {
|
||||
entity: Address::Attribute(DURATION_KEY.parse().unwrap()),
|
||||
|
@ -36,7 +34,6 @@ lazy_static! {
|
|||
value: EntryValue::Address(MEDIA_TYPE_INVARIANT.entity().unwrap()),
|
||||
provenance: "INVARIANT".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: None,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -49,7 +46,6 @@ impl Extractor for MediaExtractor {
|
|||
_connection: &UpEndConnection,
|
||||
store: Arc<Box<dyn UpStore + Send + Sync>>,
|
||||
mut job_container: JobContainer,
|
||||
context: OperationContext,
|
||||
) -> Result<Vec<Entry>> {
|
||||
if let Address::Hash(hash) = address {
|
||||
let files = store.retrieve(hash)?;
|
||||
|
@ -98,8 +94,7 @@ impl Extractor for MediaExtractor {
|
|||
entity: address.clone(),
|
||||
attribute: DURATION_KEY.parse().unwrap(),
|
||||
value: EntryValue::Number(duration),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
},
|
||||
(&MEDIA_TYPE_INVARIANT as &InvariantEntry)
|
||||
|
@ -111,8 +106,7 @@ impl Extractor for MediaExtractor {
|
|||
entity: address.clone(),
|
||||
attribute: ATTR_IN.parse().unwrap(),
|
||||
value: EntryValue::Address(MEDIA_TYPE_INVARIANT.entity().unwrap()),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -6,9 +6,7 @@ use std::{
|
|||
};
|
||||
use tracing::{debug, info, trace};
|
||||
use upend_base::{addressing::Address, entry::Entry};
|
||||
use upend_db::{
|
||||
jobs::JobContainer, stores::UpStore, OperationContext, UpEndConnection, UpEndDatabase,
|
||||
};
|
||||
use upend_db::{jobs::JobContainer, stores::UpStore, UpEndConnection, UpEndDatabase};
|
||||
|
||||
#[cfg(feature = "extractors-web")]
|
||||
pub mod web;
|
||||
|
@ -29,7 +27,6 @@ pub trait Extractor {
|
|||
connection: &UpEndConnection,
|
||||
store: Arc<Box<dyn UpStore + Send + Sync>>,
|
||||
job_container: JobContainer,
|
||||
context: OperationContext,
|
||||
) -> Result<Vec<Entry>>;
|
||||
|
||||
fn is_needed(&self, _address: &Address, _connection: &UpEndConnection) -> Result<bool> {
|
||||
|
@ -42,10 +39,9 @@ pub trait Extractor {
|
|||
connection: &UpEndConnection,
|
||||
store: Arc<Box<dyn UpStore + Send + Sync>>,
|
||||
job_container: JobContainer,
|
||||
context: OperationContext,
|
||||
) -> Result<usize> {
|
||||
if self.is_needed(address, connection)? {
|
||||
let entries = self.get(address, connection, store, job_container, context)?;
|
||||
let entries = self.get(address, connection, store, job_container)?;
|
||||
trace!("For \"{address}\", got: {entries:?}");
|
||||
|
||||
connection.transaction(|| {
|
||||
|
@ -66,7 +62,6 @@ pub fn extract_all<D: Borrow<UpEndDatabase>>(
|
|||
db: D,
|
||||
store: Arc<Box<dyn UpStore + Send + Sync>>,
|
||||
mut job_container: JobContainer,
|
||||
context: OperationContext,
|
||||
) -> Result<usize> {
|
||||
info!("Extracting metadata for all addresses.");
|
||||
|
||||
|
@ -82,13 +77,7 @@ pub fn extract_all<D: Borrow<UpEndDatabase>>(
|
|||
.par_iter()
|
||||
.map(|address| {
|
||||
let connection = db.connection()?;
|
||||
let entry_count = extract(
|
||||
address,
|
||||
&connection,
|
||||
store.clone(),
|
||||
job_container.clone(),
|
||||
context.clone(),
|
||||
);
|
||||
let entry_count = extract(address, &connection, store.clone(), job_container.clone());
|
||||
|
||||
let mut cnt = count.write().unwrap();
|
||||
*cnt += 1;
|
||||
|
@ -118,7 +107,6 @@ pub fn extract(
|
|||
connection: &UpEndConnection,
|
||||
store: Arc<Box<dyn UpStore + Send + Sync>>,
|
||||
job_container: JobContainer,
|
||||
context: OperationContext,
|
||||
) -> usize {
|
||||
let mut entry_count = 0;
|
||||
trace!("Extracting metadata for {address:?}");
|
||||
|
@ -130,7 +118,6 @@ pub fn extract(
|
|||
connection,
|
||||
store.clone(),
|
||||
job_container.clone(),
|
||||
context.clone(),
|
||||
);
|
||||
|
||||
match extract_result {
|
||||
|
@ -146,7 +133,6 @@ pub fn extract(
|
|||
connection,
|
||||
store.clone(),
|
||||
job_container.clone(),
|
||||
context.clone(),
|
||||
);
|
||||
|
||||
match extract_result {
|
||||
|
@ -162,7 +148,6 @@ pub fn extract(
|
|||
connection,
|
||||
store.clone(),
|
||||
job_container.clone(),
|
||||
context.clone(),
|
||||
);
|
||||
|
||||
match extract_result {
|
||||
|
@ -173,13 +158,8 @@ pub fn extract(
|
|||
|
||||
#[cfg(feature = "extractors-media")]
|
||||
{
|
||||
let extract_result = media::MediaExtractor.insert_info(
|
||||
address,
|
||||
connection,
|
||||
store.clone(),
|
||||
job_container,
|
||||
context.clone(),
|
||||
);
|
||||
let extract_result =
|
||||
media::MediaExtractor.insert_info(address, connection, store.clone(), job_container);
|
||||
|
||||
match extract_result {
|
||||
Ok(count) => entry_count += count,
|
||||
|
|
|
@ -14,7 +14,7 @@ use upend_base::entry::EntryValue;
|
|||
use upend_db::jobs::JobContainer;
|
||||
use upend_db::jobs::JobState;
|
||||
use upend_db::stores::UpStore;
|
||||
use upend_db::{OperationContext, UpEndConnection};
|
||||
use upend_db::UpEndConnection;
|
||||
use webpage::HTML;
|
||||
|
||||
pub struct WebExtractor;
|
||||
|
@ -26,7 +26,6 @@ impl Extractor for WebExtractor {
|
|||
_connection: &UpEndConnection,
|
||||
_store: Arc<Box<dyn UpStore + Send + Sync>>,
|
||||
mut job_container: JobContainer,
|
||||
context: OperationContext,
|
||||
) -> Result<Vec<Entry>> {
|
||||
if let Address::Url(url) = address {
|
||||
let mut job_handle =
|
||||
|
@ -43,24 +42,21 @@ impl Extractor for WebExtractor {
|
|||
entity: address.clone(),
|
||||
attribute: "HTML_TITLE".parse().unwrap(),
|
||||
value: html_title.clone().into(),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
}),
|
||||
html.title.map(|html_title| Entry {
|
||||
entity: address.clone(),
|
||||
attribute: ATTR_LABEL.parse().unwrap(),
|
||||
value: html_title.into(),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
}),
|
||||
html.description.map(|html_desc| Entry {
|
||||
entity: address.clone(),
|
||||
attribute: "HTML_DESCRIPTION".parse().unwrap(),
|
||||
value: html_desc.into(),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
}),
|
||||
];
|
||||
|
@ -71,8 +67,7 @@ impl Extractor for WebExtractor {
|
|||
entity: address.clone(),
|
||||
attribute: ATTR_LABEL.parse()?,
|
||||
value: value.clone().into(),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
}));
|
||||
}
|
||||
|
@ -81,8 +76,7 @@ impl Extractor for WebExtractor {
|
|||
entity: address.clone(),
|
||||
attribute: attribute.parse()?,
|
||||
value: value.into(),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
}));
|
||||
}
|
||||
|
@ -91,8 +85,7 @@ impl Extractor for WebExtractor {
|
|||
entity: address.clone(),
|
||||
attribute: "OG_IMAGE".parse()?,
|
||||
value: image.url.into(),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
}))
|
||||
}
|
||||
|
@ -108,8 +101,7 @@ impl Extractor for WebExtractor {
|
|||
entity: Address::Attribute(e.attribute.clone()),
|
||||
attribute: ATTR_OF.parse().unwrap(),
|
||||
value: EntryValue::Address(TYPE_URL_ADDRESS.clone()),
|
||||
provenance: context.provenance.clone() + "EXTRACTOR",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM EXTRACTOR".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
},
|
||||
e,
|
||||
|
@ -157,13 +149,7 @@ mod test {
|
|||
let address = Address::Url(Url::parse("https://upend.dev").unwrap());
|
||||
assert!(WebExtractor.is_needed(&address, &connection)?);
|
||||
|
||||
WebExtractor.insert_info(
|
||||
&address,
|
||||
&connection,
|
||||
store,
|
||||
job_container,
|
||||
OperationContext::default(),
|
||||
)?;
|
||||
WebExtractor.insert_info(&address, &connection, store, job_container)?;
|
||||
|
||||
assert!(!WebExtractor.is_needed(&address, &connection)?);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use std::collections::HashMap;
|
|||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
use tracing::trace;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing_subscriber::filter::{EnvFilter, LevelFilter};
|
||||
|
@ -26,7 +26,7 @@ use upend_base::hash::{sha256hash, UpMultihash};
|
|||
use upend_db::jobs::JobContainer;
|
||||
use upend_db::stores::fs::FsStore;
|
||||
use upend_db::stores::UpStore;
|
||||
use upend_db::{BlobMode, OperationContext, UpEndDatabase};
|
||||
use upend_db::{BlobMode, UpEndDatabase};
|
||||
|
||||
use crate::util::exec::block_background;
|
||||
|
||||
|
@ -80,7 +80,7 @@ enum Commands {
|
|||
entity: String,
|
||||
/// The attribute of the entry.
|
||||
attribute: String,
|
||||
/// The value; its type will be heuristically determined.
|
||||
/// The value; its type will be heurestically determined.
|
||||
value: String,
|
||||
/// Output format
|
||||
#[arg(short, long, default_value = "tsv")]
|
||||
|
@ -172,6 +172,10 @@ struct ServeArgs {
|
|||
#[arg(long, env = "UPEND_SECRET")]
|
||||
secret: Option<String>,
|
||||
|
||||
/// Authentication key users must supply.
|
||||
#[arg(long, env = "UPEND_KEY")]
|
||||
key: Option<String>,
|
||||
|
||||
/// Allowed host/domain name the API can serve.
|
||||
#[arg(long, env = "UPEND_ALLOW_HOST")]
|
||||
allow_host: Vec<String>,
|
||||
|
@ -345,7 +349,7 @@ async fn main() -> Result<()> {
|
|||
if !exists {
|
||||
warn!(
|
||||
"Couldn't locate Web UI directory ({:?}), disabling...",
|
||||
*WEBUI_PATH
|
||||
WEBUI_PATH.to_owned()
|
||||
);
|
||||
}
|
||||
exists
|
||||
|
@ -411,9 +415,9 @@ async fn main() -> Result<()> {
|
|||
})),
|
||||
desktop_enabled: !args.no_desktop,
|
||||
trust_executables: args.trust_executables,
|
||||
key: args.key,
|
||||
secret,
|
||||
},
|
||||
public: Arc::new(Mutex::new(upend.connection()?.get_users()?.is_empty())),
|
||||
};
|
||||
|
||||
// Start HTTP server
|
||||
|
@ -447,18 +451,13 @@ async fn main() -> Result<()> {
|
|||
block_background::<_, _, anyhow::Error>(move || {
|
||||
let connection: upend_db::UpEndConnection = upend.connection()?;
|
||||
|
||||
let tree_mode = if let Some(rescan_mode) = args.rescan_mode {
|
||||
connection.set_vault_options(upend_db::VaultOptions {
|
||||
blob_mode: Some(rescan_mode.clone()),
|
||||
})?;
|
||||
rescan_mode
|
||||
} else {
|
||||
let tree_mode = args.rescan_mode.unwrap_or_else(|| {
|
||||
connection
|
||||
.get_vault_options()
|
||||
.unwrap()
|
||||
.blob_mode
|
||||
.unwrap_or_default()
|
||||
};
|
||||
});
|
||||
|
||||
let _ = state.store.update(
|
||||
&upend,
|
||||
|
@ -467,14 +466,8 @@ async fn main() -> Result<()> {
|
|||
initial: false,
|
||||
tree_mode,
|
||||
},
|
||||
OperationContext::default(),
|
||||
);
|
||||
let _ = extractors::extract_all(
|
||||
upend,
|
||||
state.store,
|
||||
job_container,
|
||||
OperationContext::default(),
|
||||
);
|
||||
let _ = extractors::extract_all(upend, state.store, job_container);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use super::Previewable;
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use super::Previewable;
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ use serde_json::json;
|
|||
use std::collections::HashMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::io::Write;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tempfile::NamedTempFile;
|
||||
use tracing::{debug, info, trace};
|
||||
|
@ -41,7 +41,6 @@ use upend_db::jobs;
|
|||
use upend_db::stores::UpdateOptions;
|
||||
use upend_db::stores::{Blob, UpStore};
|
||||
use upend_db::BlobMode;
|
||||
use upend_db::OperationContext;
|
||||
use upend_db::UpEndDatabase;
|
||||
use upend_db::VaultOptions;
|
||||
use url::Url;
|
||||
|
@ -58,146 +57,69 @@ pub struct State {
|
|||
pub job_container: jobs::JobContainer,
|
||||
pub preview_store: Option<Arc<PreviewStore>>,
|
||||
pub preview_thread_pool: Option<Arc<rayon::ThreadPool>>,
|
||||
pub public: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct JwtClaims {
|
||||
user: String,
|
||||
exp: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UserPayload {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginQueryParams {
|
||||
via: Option<String>,
|
||||
pub struct LoginRequest {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[post("/api/auth/login")]
|
||||
pub async fn login(
|
||||
state: web::Data<State>,
|
||||
payload: web::Json<UserPayload>,
|
||||
query: web::Query<LoginQueryParams>,
|
||||
payload: web::Json<LoginRequest>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let conn = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
if state.config.key.is_none() || Some(&payload.key) == state.config.key.as_ref() {
|
||||
let claims = JwtClaims {
|
||||
exp: (SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(ErrorInternalServerError)?
|
||||
.as_secs()
|
||||
+ 7 * 24 * 60 * 60) as usize,
|
||||
};
|
||||
|
||||
match conn.authenticate_user(&payload.username, &payload.password) {
|
||||
Ok(()) => {
|
||||
let token = create_token(&payload.username, &state.config.secret)?;
|
||||
match query.via.as_deref() {
|
||||
Some("cookie") => Ok(HttpResponse::NoContent()
|
||||
.append_header((http::header::SET_COOKIE, format!("key={}; Path=/", token)))
|
||||
.finish()),
|
||||
_ => Ok(HttpResponse::Ok().json(json!({ "key": token }))),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(ErrorUnauthorized("Invalid credentials.")),
|
||||
let token = jsonwebtoken::encode(
|
||||
&jsonwebtoken::Header::default(),
|
||||
&claims,
|
||||
&jsonwebtoken::EncodingKey::from_secret(state.config.secret.as_ref()),
|
||||
)
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(json!({ "token": token })))
|
||||
} else {
|
||||
Err(ErrorUnauthorized("Incorrect token."))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/auth/logout")]
|
||||
pub async fn logout() -> Result<HttpResponse, Error> {
|
||||
Ok(HttpResponse::NoContent()
|
||||
.append_header((http::header::SET_COOKIE, "key=; Path=/; Max-Age=0"))
|
||||
.finish())
|
||||
}
|
||||
fn check_auth(req: &HttpRequest, state: &State) -> Result<(), actix_web::Error> {
|
||||
if let Some(key) = &state.config.key {
|
||||
if let Some(auth_header) = req.headers().get("Authorization") {
|
||||
let auth_header = auth_header.to_str().map_err(|err| {
|
||||
ErrorBadRequest(format!("Invalid value in Authorization header: {err:?}"))
|
||||
})?;
|
||||
|
||||
#[post("/api/auth/register")]
|
||||
pub async fn register(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
payload: web::Json<UserPayload>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
let token = jsonwebtoken::decode::<JwtClaims>(
|
||||
auth_header,
|
||||
&jsonwebtoken::DecodingKey::from_secret(key.as_ref()),
|
||||
&jsonwebtoken::Validation::default(),
|
||||
);
|
||||
|
||||
let conn = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
|
||||
match conn.set_user(&payload.username, &payload.password) {
|
||||
Ok(_) => {
|
||||
*state.public.lock().unwrap() = false;
|
||||
let token = create_token(&payload.username, &state.config.secret)?;
|
||||
Ok(HttpResponse::Ok().json(json!({ "token": token })))
|
||||
}
|
||||
Err(e) => Err(ErrorInternalServerError(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/auth/whoami")]
|
||||
pub async fn whoami(req: HttpRequest, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
let user = check_auth(&req, &state)?;
|
||||
Ok(HttpResponse::Ok().json(json!({ "user": user })))
|
||||
}
|
||||
|
||||
fn check_auth(req: &HttpRequest, state: &State) -> Result<Option<String>, actix_web::Error> {
|
||||
if *state.public.lock().unwrap() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let header_key = req.headers().get("Authorization").and_then(|value| {
|
||||
value.to_str().ok().and_then(|value| {
|
||||
if value.starts_with("Bearer ") {
|
||||
Some(value.trim_start_matches("Bearer ").to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let cookie_key = req.cookies().ok().and_then(|cookies| {
|
||||
cookies
|
||||
.iter()
|
||||
.find(|c| c.name() == "key")
|
||||
.map(|cookie| cookie.value().to_string())
|
||||
});
|
||||
|
||||
let query_key = req.query_string().split('&').find_map(|pair| {
|
||||
let parts = pair.split('=').collect::<Vec<&str>>();
|
||||
match parts[..] {
|
||||
["auth_key", value] => Some(value.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
let key = header_key.or(cookie_key).or(query_key);
|
||||
|
||||
if let Some(key) = key {
|
||||
let token = jsonwebtoken::decode::<JwtClaims>(
|
||||
&key,
|
||||
&jsonwebtoken::DecodingKey::from_secret(state.config.secret.as_ref()),
|
||||
&jsonwebtoken::Validation::default(),
|
||||
);
|
||||
match token {
|
||||
Ok(token) => Ok(Some(token.claims.user)),
|
||||
Err(err) => Err(ErrorUnauthorized(format!("Invalid token: {err:?}"))),
|
||||
token
|
||||
.map(|_| ())
|
||||
.map_err(|err| ErrorUnauthorized(format!("Invalid token: {err:?}")))
|
||||
} else {
|
||||
Err(ErrorUnauthorized("Authorization required."))
|
||||
}
|
||||
} else {
|
||||
Err(ErrorUnauthorized("Authorization required."))
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_token(username: &str, secret: &str) -> Result<String, Error> {
|
||||
let claims = JwtClaims {
|
||||
user: username.to_string(),
|
||||
exp: (SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(ErrorInternalServerError)?
|
||||
.as_secs()
|
||||
+ 7 * 24 * 60 * 60) as usize,
|
||||
};
|
||||
|
||||
jsonwebtoken::encode(
|
||||
&jsonwebtoken::Header::default(),
|
||||
&claims,
|
||||
&jsonwebtoken::EncodingKey::from_secret(secret.as_ref()),
|
||||
)
|
||||
.map_err(ErrorInternalServerError)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RawRequest {
|
||||
native: Option<String>,
|
||||
|
@ -206,13 +128,10 @@ pub struct RawRequest {
|
|||
|
||||
#[get("/api/raw/{hash}")]
|
||||
pub async fn get_raw(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
web::Query(query): web::Query<RawRequest>,
|
||||
hash: web::Path<String>,
|
||||
) -> Result<impl Responder, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
|
||||
let address =
|
||||
Address::decode(&b58_decode(hash.into_inner()).map_err(ErrorInternalServerError)?)
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
@ -299,12 +218,9 @@ pub async fn get_raw(
|
|||
|
||||
#[head("/api/raw/{hash}")]
|
||||
pub async fn head_raw(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
hash: web::Path<String>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
|
||||
let address =
|
||||
Address::decode(&b58_decode(hash.into_inner()).map_err(ErrorInternalServerError)?)
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
@ -338,13 +254,10 @@ pub async fn head_raw(
|
|||
|
||||
#[get("/api/thumb/{hash}")]
|
||||
pub async fn get_thumbnail(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
hash: web::Path<String>,
|
||||
web::Query(query): web::Query<HashMap<String, String>>,
|
||||
) -> Result<Either<NamedFile, HttpResponse>, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
|
||||
#[cfg(feature = "previews")]
|
||||
if let Some(preview_store) = &state.preview_store {
|
||||
let hash = hash.into_inner();
|
||||
|
@ -386,13 +299,7 @@ pub async fn get_thumbnail(
|
|||
}
|
||||
|
||||
#[post("/api/query")]
|
||||
pub async fn get_query(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
query: String,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
|
||||
pub async fn get_query(state: web::Data<State>, query: String) -> Result<HttpResponse, Error> {
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
|
||||
let in_query: Query = query.parse().map_err(ErrorBadRequest)?;
|
||||
|
@ -434,12 +341,9 @@ impl EntriesAsHash for Vec<Entry> {
|
|||
|
||||
#[get("/api/obj/{address_str}")]
|
||||
pub async fn get_object(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
address: web::Path<Address>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
let address = address.into_inner();
|
||||
|
||||
|
@ -481,7 +385,6 @@ pub struct InEntry {
|
|||
pub value: EntryValue,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
pub enum PutInput {
|
||||
|
@ -502,7 +405,7 @@ pub async fn put_object(
|
|||
payload: web::Json<PutInput>,
|
||||
web::Query(query): web::Query<UpdateQuery>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let user = check_auth(&req, &state)?;
|
||||
check_auth(&req, &state)?;
|
||||
|
||||
let (entry_address, entity_address) = {
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
|
@ -511,7 +414,6 @@ pub async fn put_object(
|
|||
debug!("PUTting {in_entry:?}");
|
||||
|
||||
let provenance = query.provenance.clone();
|
||||
let _user = user.clone();
|
||||
let process_inentry = move |in_entry: InEntry| -> Result<Entry> {
|
||||
if let Some(entity) = in_entry.entity {
|
||||
Ok(Entry {
|
||||
|
@ -525,7 +427,6 @@ pub async fn put_object(
|
|||
.trim()
|
||||
.to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: _user.clone(),
|
||||
})
|
||||
} else {
|
||||
Ok(Entry::try_from(&InvariantEntry {
|
||||
|
@ -567,25 +468,15 @@ pub async fn put_object(
|
|||
let _address = address.clone();
|
||||
let _job_container = state.job_container.clone();
|
||||
let _store = state.store.clone();
|
||||
let _user = user.clone();
|
||||
block_background::<_, _, anyhow::Error>(move || {
|
||||
let entry_count = extractors::extract(
|
||||
&_address,
|
||||
&connection,
|
||||
_store,
|
||||
_job_container,
|
||||
OperationContext {
|
||||
user: _user,
|
||||
provenance: "API".to_string(),
|
||||
},
|
||||
);
|
||||
let entry_count =
|
||||
extractors::extract(&_address, &connection, _store, _job_container);
|
||||
|
||||
debug!("Added {entry_count} extracted entries for {_address:?}");
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
let _user = user.clone();
|
||||
web::block(move || {
|
||||
connection.transaction::<_, anyhow::Error, _>(|| {
|
||||
if connection.retrieve_object(&address)?.is_empty() {
|
||||
|
@ -604,7 +495,6 @@ pub async fn put_object(
|
|||
})
|
||||
.trim()
|
||||
.to_string(),
|
||||
user: _user,
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
})?;
|
||||
}
|
||||
|
@ -627,7 +517,7 @@ pub async fn put_blob(
|
|||
state: web::Data<State>,
|
||||
mut payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let user = check_auth(&req, &state)?;
|
||||
check_auth(&req, &state)?;
|
||||
|
||||
if let Some(mut field) = payload.try_next().await? {
|
||||
let mut file = NamedTempFile::new()?;
|
||||
|
@ -666,7 +556,6 @@ pub async fn put_blob(
|
|||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
let _store = state.store.clone();
|
||||
let _filename = filename.clone();
|
||||
let _user = user.clone();
|
||||
let hash = web::block(move || {
|
||||
let options = connection.get_vault_options()?;
|
||||
_store
|
||||
|
@ -675,10 +564,6 @@ pub async fn put_blob(
|
|||
Blob::from_filepath(file.path()),
|
||||
_filename,
|
||||
options.blob_mode,
|
||||
OperationContext {
|
||||
user: _user,
|
||||
provenance: "API".to_string(),
|
||||
},
|
||||
)
|
||||
.map_err(anyhow::Error::from)
|
||||
})
|
||||
|
@ -704,18 +589,8 @@ pub async fn put_blob(
|
|||
let _job_container = state.job_container.clone();
|
||||
let _store = state.store.clone();
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
let _user = user.clone();
|
||||
block_background::<_, _, anyhow::Error>(move || {
|
||||
let entry_count = extractors::extract(
|
||||
&_address,
|
||||
&connection,
|
||||
_store,
|
||||
_job_container,
|
||||
OperationContext {
|
||||
user: _user,
|
||||
provenance: "API".to_string(),
|
||||
},
|
||||
);
|
||||
let entry_count = extractors::extract(&_address, &connection, _store, _job_container);
|
||||
debug!("Added {entry_count} extracted entries for {_address:?}");
|
||||
Ok(())
|
||||
});
|
||||
|
@ -733,7 +608,7 @@ pub async fn put_object_attribute(
|
|||
value: web::Json<EntryValue>,
|
||||
web::Query(query): web::Query<UpdateQuery>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let user = check_auth(&req, &state)?;
|
||||
check_auth(&req, &state)?;
|
||||
let (address, attribute) = path.into_inner();
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
|
||||
|
@ -756,7 +631,6 @@ pub async fn put_object_attribute(
|
|||
})
|
||||
.trim()
|
||||
.to_string(),
|
||||
user: user.clone(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
};
|
||||
|
||||
|
@ -861,12 +735,7 @@ pub async fn get_address(
|
|||
}
|
||||
|
||||
#[get("/api/all/attributes")]
|
||||
pub async fn get_all_attributes(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
|
||||
pub async fn get_all_attributes(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
let attributes = web::block(move || connection.get_all_attributes())
|
||||
.await?
|
||||
|
@ -909,8 +778,6 @@ pub async fn list_hier(
|
|||
path: web::Path<String>,
|
||||
req: HttpRequest,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let user = check_auth(&req, &state)?;
|
||||
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
if path.is_empty() {
|
||||
Ok(HttpResponse::MovedPermanently()
|
||||
|
@ -921,19 +788,9 @@ pub async fn list_hier(
|
|||
trace!(r#"Listing path "{}""#, upath);
|
||||
|
||||
let create = !req.method().is_safe();
|
||||
let path = web::block(move || {
|
||||
resolve_path(
|
||||
&connection,
|
||||
&upath,
|
||||
create,
|
||||
OperationContext {
|
||||
user,
|
||||
provenance: "API".to_string(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.await?
|
||||
.map_err(ErrorNotFound)?;
|
||||
let path = web::block(move || resolve_path(&connection, &upath, create))
|
||||
.await?
|
||||
.map_err(ErrorNotFound)?;
|
||||
match path.last() {
|
||||
Some(addr) => Ok(HttpResponse::Found()
|
||||
.append_header((http::header::LOCATION, format!("../../api/obj/{}", addr)))
|
||||
|
@ -944,11 +801,7 @@ pub async fn list_hier(
|
|||
}
|
||||
|
||||
#[get("/api/hier_roots")]
|
||||
pub async fn list_hier_roots(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
pub async fn list_hier_roots(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
|
||||
let result = web::block(move || {
|
||||
|
@ -976,7 +829,7 @@ pub async fn api_refresh(
|
|||
state: web::Data<State>,
|
||||
web::Query(query): web::Query<RescanRequest>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let user = check_auth(&req, &state)?;
|
||||
check_auth(&req, &state)?;
|
||||
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
|
||||
|
@ -993,19 +846,11 @@ pub async fn api_refresh(
|
|||
.unwrap_or_default(),
|
||||
),
|
||||
},
|
||||
OperationContext {
|
||||
user: user.clone(),
|
||||
provenance: "API".to_string(),
|
||||
},
|
||||
);
|
||||
let _ = crate::extractors::extract_all(
|
||||
state.upend.clone(),
|
||||
state.store.clone(),
|
||||
state.job_container.clone(),
|
||||
OperationContext {
|
||||
user: user.clone(),
|
||||
provenance: "API".to_string(),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
});
|
||||
|
@ -1013,15 +858,13 @@ pub async fn api_refresh(
|
|||
}
|
||||
|
||||
#[get("/api/stats/vault")]
|
||||
pub async fn vault_stats(req: HttpRequest, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
pub async fn vault_stats(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
Ok(HttpResponse::Ok().json(connection.get_stats().map_err(ErrorInternalServerError)?))
|
||||
}
|
||||
|
||||
#[get("/api/stats/store")]
|
||||
pub async fn store_stats(req: HttpRequest, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
pub async fn store_stats(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
Ok(HttpResponse::Ok().json(json!({
|
||||
"main": state.store.stats().map_err(ErrorInternalServerError)?
|
||||
})))
|
||||
|
@ -1034,11 +877,9 @@ pub struct JobsRequest {
|
|||
|
||||
#[get("/api/jobs")]
|
||||
pub async fn get_jobs(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
web::Query(query): web::Query<JobsRequest>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
let jobs = state
|
||||
.job_container
|
||||
.get_jobs()
|
||||
|
@ -1065,14 +906,12 @@ pub async fn get_info(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
|||
upend_db::common::build::PKG_VERSION,
|
||||
build::PKG_VERSION
|
||||
),
|
||||
"desktop": state.config.desktop_enabled,
|
||||
"public": *state.public.lock().unwrap(),
|
||||
"desktop": state.config.desktop_enabled
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/api/options")]
|
||||
pub async fn get_options(req: HttpRequest, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
pub async fn get_options(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
Ok(HttpResponse::Ok().json(
|
||||
connection
|
||||
|
@ -1100,11 +939,7 @@ pub async fn put_options(
|
|||
}
|
||||
|
||||
#[get("/api/migration/user-entries")]
|
||||
pub async fn get_user_entries(
|
||||
req: HttpRequest,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
check_auth(&req, &state)?;
|
||||
pub async fn get_user_entries(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||
|
||||
let result = web::block(move || connection.get_explicit_entries())
|
||||
|
@ -1379,7 +1214,6 @@ mod tests {
|
|||
initial: true,
|
||||
tree_mode: upend_db::BlobMode::default(),
|
||||
},
|
||||
OperationContext::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -1391,11 +1225,11 @@ mod tests {
|
|||
desktop_enabled: false,
|
||||
trust_executables: false,
|
||||
secret: "secret".to_string(),
|
||||
key: None,
|
||||
},
|
||||
job_container,
|
||||
preview_store: None,
|
||||
preview_thread_pool: None,
|
||||
public: Arc::new(Mutex::new(true)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,9 +46,6 @@ where
|
|||
.app_data(actix_web::web::Data::new(state))
|
||||
.wrap(actix_web::middleware::Logger::default().exclude("/api/jobs"))
|
||||
.service(routes::login)
|
||||
.service(routes::register)
|
||||
.service(routes::logout)
|
||||
.service(routes::whoami)
|
||||
.service(routes::get_raw)
|
||||
.service(routes::head_raw)
|
||||
.service(routes::get_thumbnail)
|
||||
|
|
|
@ -26,16 +26,13 @@ once_cell = "1.7.2"
|
|||
lru = "0.7.0"
|
||||
|
||||
diesel = { version = "1.4", features = [
|
||||
"sqlite",
|
||||
"r2d2",
|
||||
"chrono",
|
||||
"serde_json",
|
||||
"sqlite",
|
||||
"r2d2",
|
||||
"chrono",
|
||||
"serde_json",
|
||||
] }
|
||||
diesel_migrations = "1.4"
|
||||
libsqlite3-sys = { version = "^0", features = ["bundled"] }
|
||||
password-hash = "0.5.0"
|
||||
argon2 = "0.5.3"
|
||||
|
||||
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -45,10 +42,10 @@ regex = "1"
|
|||
|
||||
multibase = "0.9"
|
||||
multihash = { version = "*", default-features = false, features = [
|
||||
"alloc",
|
||||
"multihash-impl",
|
||||
"sha2",
|
||||
"identity",
|
||||
"alloc",
|
||||
"multihash-impl",
|
||||
"sha2",
|
||||
"identity",
|
||||
] }
|
||||
uuid = { version = "1.4", features = ["v4"] }
|
||||
url = { version = "2", features = ["serde"] }
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE users;
|
|
@ -1,7 +0,0 @@
|
|||
CREATE TABLE users
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
username VARCHAR NOT NULL,
|
||||
password VARCHAR NOT NULL,
|
||||
UNIQUE (username)
|
||||
);
|
|
@ -1,2 +0,0 @@
|
|||
ALTER TABLE data
|
||||
DROP COLUMN user;
|
|
@ -1,2 +0,0 @@
|
|||
ALTER TABLE data
|
||||
ADD COLUMN user VARCHAR;
|
|
@ -178,7 +178,9 @@ pub fn execute(
|
|||
var_name.clone(),
|
||||
subquery_results
|
||||
.iter()
|
||||
.map(|e| e.attribute.parse().map(EntryPart::Attribute))
|
||||
.map(|e| {
|
||||
e.attribute.parse().map(|a| EntryPart::Attribute(a))
|
||||
})
|
||||
.collect::<Result<Vec<EntryPart>, _>>()?,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ impl TryFrom<&models::Entry> for Entry {
|
|||
attribute: e.attribute.parse()?,
|
||||
value: value_str.parse().unwrap(),
|
||||
provenance: e.provenance.clone(),
|
||||
user: e.user.clone(),
|
||||
timestamp: e.timestamp,
|
||||
})
|
||||
} else if let Some(value_num) = e.value_num {
|
||||
|
@ -23,7 +22,6 @@ impl TryFrom<&models::Entry> for Entry {
|
|||
attribute: e.attribute.parse()?,
|
||||
value: EntryValue::Number(value_num),
|
||||
provenance: e.provenance.clone(),
|
||||
user: e.user.clone(),
|
||||
timestamp: e.timestamp,
|
||||
})
|
||||
} else {
|
||||
|
@ -32,7 +30,6 @@ impl TryFrom<&models::Entry> for Entry {
|
|||
attribute: e.attribute.parse()?,
|
||||
value: EntryValue::Number(f64::NAN),
|
||||
provenance: e.provenance.clone(),
|
||||
user: e.user.clone(),
|
||||
timestamp: e.timestamp,
|
||||
})
|
||||
}
|
||||
|
@ -56,7 +53,6 @@ impl TryFrom<&Entry> for models::Entry {
|
|||
value_num: None,
|
||||
immutable: false,
|
||||
provenance: e.provenance.clone(),
|
||||
user: e.user.clone(),
|
||||
timestamp: e.timestamp,
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ use lru::LruCache;
|
|||
use tracing::trace;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::OperationContext;
|
||||
use upend_base::addressing::Address;
|
||||
use upend_base::constants::ATTR_LABEL;
|
||||
use upend_base::constants::{ATTR_IN, HIER_ROOT_ADDR, HIER_ROOT_INVARIANT};
|
||||
|
@ -92,7 +91,6 @@ pub fn fetch_or_create_dir(
|
|||
parent: Option<Address>,
|
||||
directory: UNode,
|
||||
create: bool,
|
||||
context: OperationContext,
|
||||
) -> Result<Address> {
|
||||
match parent.clone() {
|
||||
Some(address) => trace!("FETCHING/CREATING {}/{:#}", address, directory),
|
||||
|
@ -139,8 +137,7 @@ pub fn fetch_or_create_dir(
|
|||
entity: new_directory_address.clone(),
|
||||
attribute: ATTR_LABEL.parse().unwrap(),
|
||||
value: directory.to_string().into(),
|
||||
provenance: context.provenance.clone() + "HIER",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM FS".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
};
|
||||
connection.insert_entry(directory_entry)?;
|
||||
|
@ -150,8 +147,7 @@ pub fn fetch_or_create_dir(
|
|||
entity: new_directory_address.clone(),
|
||||
attribute: ATTR_IN.parse().unwrap(),
|
||||
value: parent.into(),
|
||||
provenance: context.provenance.clone() + "HIER",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM FS".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
}
|
||||
} else {
|
||||
|
@ -159,8 +155,7 @@ pub fn fetch_or_create_dir(
|
|||
entity: new_directory_address.clone(),
|
||||
attribute: ATTR_IN.parse().unwrap(),
|
||||
value: HIER_ROOT_ADDR.clone().into(),
|
||||
provenance: context.provenance.clone() + "HIER",
|
||||
user: context.user.clone(),
|
||||
provenance: "SYSTEM FS".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
}
|
||||
})?;
|
||||
|
@ -182,7 +177,6 @@ pub fn resolve_path(
|
|||
connection: &UpEndConnection,
|
||||
path: &UHierPath,
|
||||
create: bool,
|
||||
context: OperationContext,
|
||||
) -> Result<Vec<Address>> {
|
||||
let mut result: Vec<Address> = vec![];
|
||||
let mut path_stack = path.0.to_vec();
|
||||
|
@ -194,7 +188,6 @@ pub fn resolve_path(
|
|||
result.last().cloned(),
|
||||
path_stack.pop().unwrap(),
|
||||
create,
|
||||
context.clone(),
|
||||
)?;
|
||||
result.push(dir_address);
|
||||
}
|
||||
|
@ -208,7 +201,6 @@ pub fn resolve_path_cached(
|
|||
connection: &UpEndConnection,
|
||||
path: &UHierPath,
|
||||
create: bool,
|
||||
context: OperationContext,
|
||||
cache: &Arc<Mutex<ResolveCache>>,
|
||||
) -> Result<Vec<Address>> {
|
||||
let mut result: Vec<Address> = vec![];
|
||||
|
@ -224,7 +216,7 @@ pub fn resolve_path_cached(
|
|||
result.push(address.clone());
|
||||
} else {
|
||||
drop(cache_lock);
|
||||
let address = fetch_or_create_dir(connection, parent, node, create, context.clone())?;
|
||||
let address = fetch_or_create_dir(connection, parent, node, create)?;
|
||||
result.push(address.clone());
|
||||
cache.lock().unwrap().put(key, address);
|
||||
}
|
||||
|
@ -294,23 +286,11 @@ mod tests {
|
|||
let open_result = UpEndDatabase::open(&temp_dir, true).unwrap();
|
||||
let connection = open_result.db.connection().unwrap();
|
||||
|
||||
let foo_result = fetch_or_create_dir(
|
||||
&connection,
|
||||
None,
|
||||
UNode("foo".to_string()),
|
||||
true,
|
||||
OperationContext::default(),
|
||||
);
|
||||
let foo_result = fetch_or_create_dir(&connection, None, UNode("foo".to_string()), true);
|
||||
assert!(foo_result.is_ok());
|
||||
let foo_result = foo_result.unwrap();
|
||||
|
||||
let bar_result = fetch_or_create_dir(
|
||||
&connection,
|
||||
None,
|
||||
UNode("bar".to_string()),
|
||||
true,
|
||||
OperationContext::default(),
|
||||
);
|
||||
let bar_result = fetch_or_create_dir(&connection, None, UNode("bar".to_string()), true);
|
||||
assert!(bar_result.is_ok());
|
||||
let bar_result = bar_result.unwrap();
|
||||
|
||||
|
@ -319,7 +299,6 @@ mod tests {
|
|||
Some(bar_result.clone()),
|
||||
UNode("baz".to_string()),
|
||||
true,
|
||||
OperationContext::default(),
|
||||
);
|
||||
assert!(baz_result.is_ok());
|
||||
let baz_result = baz_result.unwrap();
|
||||
|
@ -327,12 +306,7 @@ mod tests {
|
|||
let roots = list_roots(&connection);
|
||||
assert_eq!(roots.unwrap(), [foo_result, bar_result.clone()]);
|
||||
|
||||
let resolve_result = resolve_path(
|
||||
&connection,
|
||||
&"bar/baz".parse().unwrap(),
|
||||
false,
|
||||
OperationContext::default(),
|
||||
);
|
||||
let resolve_result = resolve_path(&connection, &"bar/baz".parse().unwrap(), false);
|
||||
|
||||
assert!(resolve_result.is_ok());
|
||||
assert_eq!(
|
||||
|
@ -340,20 +314,10 @@ mod tests {
|
|||
vec![bar_result.clone(), baz_result.clone()]
|
||||
);
|
||||
|
||||
let resolve_result = resolve_path(
|
||||
&connection,
|
||||
&"bar/baz/bax".parse().unwrap(),
|
||||
false,
|
||||
OperationContext::default(),
|
||||
);
|
||||
let resolve_result = resolve_path(&connection, &"bar/baz/bax".parse().unwrap(), false);
|
||||
assert!(resolve_result.is_err());
|
||||
|
||||
let resolve_result = resolve_path(
|
||||
&connection,
|
||||
&"bar/baz/bax".parse().unwrap(),
|
||||
true,
|
||||
OperationContext::default(),
|
||||
);
|
||||
let resolve_result = resolve_path(&connection, &"bar/baz/bax".parse().unwrap(), true);
|
||||
assert!(resolve_result.is_ok());
|
||||
|
||||
let bax_result = fetch_or_create_dir(
|
||||
|
@ -361,7 +325,6 @@ mod tests {
|
|||
Some(baz_result.clone()),
|
||||
UNode("bax".to_string()),
|
||||
false,
|
||||
OperationContext::default(),
|
||||
);
|
||||
assert!(bax_result.is_ok());
|
||||
let bax_result = bax_result.unwrap();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::schema::{data, meta, users};
|
||||
use super::schema::{data, meta};
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::Serialize;
|
||||
|
||||
|
@ -13,7 +13,6 @@ pub struct Entry {
|
|||
pub value_num: Option<f64>,
|
||||
pub immutable: bool,
|
||||
pub provenance: String,
|
||||
pub user: Option<String>,
|
||||
pub timestamp: NaiveDateTime,
|
||||
}
|
||||
|
||||
|
@ -24,11 +23,3 @@ pub struct MetaValue {
|
|||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Insertable, Serialize, Clone, Debug)]
|
||||
#[table_name = "users"]
|
||||
pub struct UserValue {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ table! {
|
|||
value_num -> Nullable<Double>,
|
||||
immutable -> Bool,
|
||||
provenance -> Text,
|
||||
user -> Nullable<Text>,
|
||||
timestamp -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
@ -21,10 +20,4 @@ table! {
|
|||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (id) {
|
||||
id -> Integer,
|
||||
username -> Text,
|
||||
password -> Text,
|
||||
}
|
||||
}
|
||||
allow_tables_to_appear_in_same_query!(data, meta,);
|
||||
|
|
101
db/src/lib.rs
101
db/src/lib.rs
|
@ -26,7 +26,6 @@ use crate::inner::models;
|
|||
use crate::inner::schema::data;
|
||||
use crate::util::LoggerSink;
|
||||
use anyhow::{anyhow, Result};
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use diesel::prelude::*;
|
||||
use diesel::r2d2::{self, ConnectionManager};
|
||||
use diesel::result::{DatabaseErrorKind, Error};
|
||||
|
@ -117,7 +116,7 @@ pub const DATABASE_FILENAME: &str = "upend.sqlite3";
|
|||
|
||||
impl UpEndDatabase {
|
||||
pub fn open<P: AsRef<Path>>(dirpath: P, reinitialize: bool) -> Result<OpenResult> {
|
||||
embed_migrations!("./migrations/upend");
|
||||
embed_migrations!("./migrations/upend/");
|
||||
|
||||
let upend_path = dirpath.as_ref().join(UPEND_SUBDIR);
|
||||
|
||||
|
@ -274,63 +273,6 @@ impl UpEndConnection {
|
|||
Ok(VaultOptions { blob_mode })
|
||||
}
|
||||
|
||||
pub fn get_users(&self) -> Result<Vec<String>> {
|
||||
use crate::inner::schema::users::dsl;
|
||||
|
||||
let _lock = self.lock.read().unwrap();
|
||||
let conn = self.pool.get()?;
|
||||
|
||||
let result = dsl::users.select(dsl::username).load::<String>(&conn)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn set_user(&self, username: &str, password: &str) -> Result<bool> {
|
||||
use crate::inner::schema::users::dsl;
|
||||
|
||||
let salt = password_hash::SaltString::generate(&mut password_hash::rand_core::OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let hashed_password = argon2
|
||||
.hash_password(password.as_ref(), &salt)
|
||||
.map_err(|e| anyhow!(e))?
|
||||
.to_string();
|
||||
|
||||
let _lock = self.lock.write().unwrap();
|
||||
let conn = self.pool.get()?;
|
||||
let result = diesel::replace_into(dsl::users)
|
||||
.values((
|
||||
dsl::username.eq(username),
|
||||
dsl::password.eq(hashed_password),
|
||||
))
|
||||
.execute(&conn)?;
|
||||
Ok(result > 0)
|
||||
}
|
||||
|
||||
pub fn authenticate_user(&self, username: &str, password: &str) -> Result<()> {
|
||||
use crate::inner::schema::users::dsl;
|
||||
|
||||
let conn = self.pool.get()?;
|
||||
let user_result = dsl::users
|
||||
.filter(dsl::username.eq(username))
|
||||
.load::<models::UserValue>(&conn)?;
|
||||
|
||||
match user_result.first() {
|
||||
Some(user) => {
|
||||
let parsed_hash = PasswordHash::new(&user.password).map_err(|e| anyhow!(e))?;
|
||||
let argon2 = Argon2::default();
|
||||
argon2
|
||||
.verify_password(password.as_ref(), &parsed_hash)
|
||||
.map_err(|e| anyhow!(e))
|
||||
}
|
||||
None => {
|
||||
let argon2 = Argon2::default();
|
||||
let _ = argon2
|
||||
.verify_password(password.as_ref(), &PasswordHash::new(&DUMMY_HASH).unwrap());
|
||||
Err(anyhow!("user not found"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retrieve_entry(&self, hash: &UpMultihash) -> Result<Option<Entry>> {
|
||||
use crate::inner::schema::data::dsl::*;
|
||||
|
||||
|
@ -546,16 +488,6 @@ impl UpEndConnection {
|
|||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref DUMMY_HASH: String = Argon2::default()
|
||||
.hash_password(
|
||||
"password".as_ref(),
|
||||
&password_hash::SaltString::generate(&mut password_hash::rand_core::OsRng)
|
||||
)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use upend_base::constants::{ATTR_IN, ATTR_LABEL};
|
||||
|
@ -670,22 +602,6 @@ mod test {
|
|||
assert_eq!(result[0].entity, edge_entity);
|
||||
assert_eq!(result[0].value, EntryValue::Address(random_entity));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_users() {
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let result = UpEndDatabase::open(&tempdir, false).unwrap();
|
||||
let db = result.db;
|
||||
|
||||
let connection = db.connection().unwrap();
|
||||
|
||||
assert!(connection.authenticate_user("thm", "hunter2").is_err());
|
||||
connection.set_user("thm", "hunter2").unwrap();
|
||||
connection.authenticate_user("thm", "hunter2").unwrap();
|
||||
assert!(connection.authenticate_user("thm", "password").is_err());
|
||||
connection.set_user("thm", "password").unwrap();
|
||||
connection.authenticate_user("thm", "password").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
@ -721,18 +637,3 @@ impl std::str::FromStr for BlobMode {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OperationContext {
|
||||
pub user: Option<String>,
|
||||
pub provenance: String,
|
||||
}
|
||||
|
||||
impl Default for OperationContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
user: None,
|
||||
provenance: "SYSTEM".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ macro_rules! upend_insert_val {
|
|||
attribute: $attribute.parse().unwrap(),
|
||||
value: upend_base::entry::EntryValue::String(String::from($value)),
|
||||
provenance: "SYSTEM INIT".to_string(),
|
||||
user: None,
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
})
|
||||
}};
|
||||
|
@ -20,7 +19,6 @@ macro_rules! upend_insert_addr {
|
|||
attribute: $attribute.parse().unwrap(),
|
||||
value: upend_base::entry::EntryValue::Address($addr.clone()),
|
||||
provenance: "SYSTEM INIT".to_string(),
|
||||
user: None,
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
})
|
||||
}};
|
||||
|
|
|
@ -5,8 +5,7 @@ use crate::hierarchies::{resolve_path, resolve_path_cached, ResolveCache, UHierP
|
|||
use crate::jobs::{JobContainer, JobHandle};
|
||||
use crate::util::hash_at_path;
|
||||
use crate::{
|
||||
BlobMode, ConnectionOptions, LoggingHandler, OperationContext, UpEndConnection, UpEndDatabase,
|
||||
UPEND_SUBDIR,
|
||||
BlobMode, ConnectionOptions, LoggingHandler, UpEndConnection, UpEndDatabase, UPEND_SUBDIR,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::prelude::*;
|
||||
|
@ -96,7 +95,6 @@ impl FsStore {
|
|||
db: D,
|
||||
job_handle: JobHandle,
|
||||
options: UpdateOptions,
|
||||
context: OperationContext,
|
||||
) -> Result<Vec<UpdatePathOutcome>> {
|
||||
let start = Instant::now();
|
||||
info!("Vault rescan started.");
|
||||
|
@ -138,7 +136,6 @@ impl FsStore {
|
|||
|
||||
// Actual processing
|
||||
let count = RwLock::new(0_usize);
|
||||
#[allow(clippy::type_complexity)]
|
||||
let resolve_cache: Arc<Mutex<LruCache<(Option<Address>, UNode), Address>>> =
|
||||
Arc::new(Mutex::new(LruCache::new(256)));
|
||||
let total = paths.len() as f32;
|
||||
|
@ -155,7 +152,6 @@ impl FsStore {
|
|||
&existing_files,
|
||||
&resolve_cache,
|
||||
quick_check,
|
||||
context.clone(),
|
||||
);
|
||||
|
||||
let mut cnt = count.write().unwrap();
|
||||
|
@ -242,7 +238,6 @@ impl FsStore {
|
|||
Ok(all_outcomes)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn process_directory_entry<D: Borrow<UpEndDatabase>>(
|
||||
&self,
|
||||
db: D,
|
||||
|
@ -252,7 +247,6 @@ impl FsStore {
|
|||
existing_files: &Arc<RwLock<Vec<db::File>>>,
|
||||
resolve_cache: &Arc<Mutex<ResolveCache>>,
|
||||
quick_check: bool,
|
||||
context: OperationContext,
|
||||
) -> Result<UpdatePathOutcome> {
|
||||
trace!("Processing: {:?}", path);
|
||||
|
||||
|
@ -370,7 +364,6 @@ impl FsStore {
|
|||
size,
|
||||
mtime,
|
||||
Some(resolve_cache),
|
||||
context,
|
||||
)
|
||||
.map(|_| {
|
||||
info!("Added: {:?}", path);
|
||||
|
@ -427,7 +420,6 @@ impl FsStore {
|
|||
size: i64,
|
||||
mtime: Option<NaiveDateTime>,
|
||||
resolve_cache: Option<&Arc<Mutex<ResolveCache>>>,
|
||||
context: OperationContext,
|
||||
) -> Result<Address> {
|
||||
let normalized_path = self.normalize_path(path)?;
|
||||
let new_file = db::NewFile {
|
||||
|
@ -450,7 +442,6 @@ impl FsStore {
|
|||
value: (size as f64).into(),
|
||||
provenance: "SYSTEM INIT".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: context.user.clone(),
|
||||
};
|
||||
|
||||
let mime_type = tree_magic_mini::from_filepath(path).map(|s| s.to_string());
|
||||
|
@ -460,7 +451,6 @@ impl FsStore {
|
|||
value: mime_type.into(),
|
||||
provenance: "SYSTEM INIT".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: context.user.clone(),
|
||||
});
|
||||
|
||||
let added_entry = Entry {
|
||||
|
@ -473,7 +463,6 @@ impl FsStore {
|
|||
.into(),
|
||||
provenance: "SYSTEM INIT".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: context.user.clone(),
|
||||
};
|
||||
|
||||
let components = normalized_path.components().collect::<Vec<Component>>();
|
||||
|
@ -497,16 +486,13 @@ impl FsStore {
|
|||
.into(),
|
||||
provenance: "SYSTEM INIT".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: context.user.clone(),
|
||||
};
|
||||
let label_entry_addr = connection.insert_entry(label_entry)?;
|
||||
|
||||
if let Some(upath) = upath {
|
||||
let resolved_path = match resolve_cache {
|
||||
Some(cache) => {
|
||||
resolve_path_cached(connection, &upath, true, context.clone(), cache)?
|
||||
}
|
||||
None => resolve_path(connection, &upath, true, context.clone())?,
|
||||
Some(cache) => resolve_path_cached(connection, &upath, true, cache)?,
|
||||
None => resolve_path(connection, &upath, true)?,
|
||||
};
|
||||
let parent_dir = resolved_path.last().unwrap();
|
||||
|
||||
|
@ -516,7 +502,6 @@ impl FsStore {
|
|||
value: parent_dir.clone().into(),
|
||||
provenance: "SYSTEM INIT".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: context.user.clone(),
|
||||
};
|
||||
let dir_has_entry_addr = connection.insert_entry(dir_has_entry)?;
|
||||
|
||||
|
@ -526,7 +511,6 @@ impl FsStore {
|
|||
value: label_entry_addr.into(),
|
||||
provenance: "SYSTEM INIT".to_string(),
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
user: context.user.clone(),
|
||||
};
|
||||
connection.insert_entry(alias_entry)?;
|
||||
}
|
||||
|
@ -665,7 +649,6 @@ impl UpStore for FsStore {
|
|||
blob: Blob,
|
||||
name_hint: Option<String>,
|
||||
blob_mode: Option<BlobMode>,
|
||||
context: OperationContext,
|
||||
) -> Result<UpMultihash, super::StoreError> {
|
||||
let file_path = blob.get_file_path();
|
||||
let hash = hash_at_path(file_path).map_err(|e| StoreError::Unknown(e.to_string()))?;
|
||||
|
@ -719,7 +702,6 @@ impl UpStore for FsStore {
|
|||
size,
|
||||
mtime,
|
||||
None,
|
||||
context,
|
||||
)
|
||||
.map_err(|e| StoreError::Unknown(e.to_string()))?;
|
||||
}
|
||||
|
@ -732,7 +714,6 @@ impl UpStore for FsStore {
|
|||
db: &UpEndDatabase,
|
||||
mut job_container: JobContainer,
|
||||
options: UpdateOptions,
|
||||
context: OperationContext,
|
||||
) -> Result<Vec<UpdatePathOutcome>, StoreError> {
|
||||
trace!(
|
||||
"Running a vault update of {:?}, options = {:?}.",
|
||||
|
@ -743,7 +724,7 @@ impl UpStore for FsStore {
|
|||
|
||||
match job_result {
|
||||
Ok(job_handle) => {
|
||||
let result = self.rescan_vault(db, job_handle, options, context);
|
||||
let result = self.rescan_vault(db, job_handle, options);
|
||||
|
||||
if let Err(err) = &result {
|
||||
error!("Update did not succeed! {:?}", err);
|
||||
|
@ -853,7 +834,6 @@ mod test {
|
|||
initial: true,
|
||||
tree_mode: BlobMode::default(),
|
||||
},
|
||||
OperationContext::default(),
|
||||
);
|
||||
assert!(rescan_result.is_ok());
|
||||
}
|
||||
|
@ -900,7 +880,6 @@ mod test {
|
|||
initial: quick,
|
||||
tree_mode: BlobMode::default(),
|
||||
},
|
||||
OperationContext::default(),
|
||||
);
|
||||
|
||||
assert!(rescan_result.is_ok());
|
||||
|
@ -921,7 +900,6 @@ mod test {
|
|||
initial: quick,
|
||||
tree_mode: BlobMode::default(),
|
||||
},
|
||||
OperationContext::default(),
|
||||
);
|
||||
|
||||
assert!(rescan_result.is_ok());
|
||||
|
@ -945,7 +923,6 @@ mod test {
|
|||
initial: quick,
|
||||
tree_mode: BlobMode::default(),
|
||||
},
|
||||
OperationContext::default(),
|
||||
);
|
||||
|
||||
assert!(rescan_result.is_ok());
|
||||
|
@ -998,7 +975,6 @@ mod test {
|
|||
initial: quick,
|
||||
tree_mode: BlobMode::default(),
|
||||
},
|
||||
OperationContext::default(),
|
||||
);
|
||||
|
||||
assert!(rescan_result.is_ok());
|
||||
|
@ -1101,7 +1077,6 @@ mod test {
|
|||
initial: true,
|
||||
tree_mode,
|
||||
},
|
||||
OperationContext::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -1112,7 +1087,7 @@ mod test {
|
|||
paths.iter().for_each(|path| {
|
||||
let upath: UHierPath = path.parse().unwrap();
|
||||
assert!(
|
||||
resolve_path(&connection, &upath, false, OperationContext::default()).is_ok(),
|
||||
resolve_path(&connection, &upath, false).is_ok(),
|
||||
"Failed: {}",
|
||||
upath
|
||||
);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{UpEndConnection, UpEndDatabase};
|
||||
use crate::OperationContext;
|
||||
use crate::{jobs::JobContainer, BlobMode};
|
||||
use upend_base::hash::UpMultihash;
|
||||
|
||||
|
@ -62,14 +61,12 @@ pub trait UpStore {
|
|||
blob: Blob,
|
||||
name_hint: Option<String>,
|
||||
blob_mode: Option<BlobMode>,
|
||||
context: OperationContext,
|
||||
) -> Result<UpMultihash>;
|
||||
fn update(
|
||||
&self,
|
||||
database: &UpEndDatabase,
|
||||
job_container: JobContainer,
|
||||
options: UpdateOptions,
|
||||
context: OperationContext,
|
||||
) -> Result<Vec<UpdatePathOutcome>>;
|
||||
fn stats(&self) -> Result<serde_json::Value>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
devDependencies:
|
||||
concurrently:
|
||||
specifier: ^8.2.2
|
||||
version: 8.2.2
|
||||
|
||||
packages:
|
||||
|
||||
/@babel/runtime@7.23.9:
|
||||
resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
dev: true
|
||||
|
||||
/ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
dev: true
|
||||
|
||||
/chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
dev: true
|
||||
|
||||
/cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
dev: true
|
||||
|
||||
/color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
dev: true
|
||||
|
||||
/color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
dev: true
|
||||
|
||||
/concurrently@8.2.2:
|
||||
resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==}
|
||||
engines: {node: ^14.13.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
date-fns: 2.30.0
|
||||
lodash: 4.17.21
|
||||
rxjs: 7.8.1
|
||||
shell-quote: 1.8.1
|
||||
spawn-command: 0.0.2
|
||||
supports-color: 8.1.1
|
||||
tree-kill: 1.2.2
|
||||
yargs: 17.7.2
|
||||
dev: true
|
||||
|
||||
/date-fns@2.30.0:
|
||||
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
||||
engines: {node: '>=0.11'}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.9
|
||||
dev: true
|
||||
|
||||
/emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
dev: true
|
||||
|
||||
/escalade@3.1.2:
|
||||
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
dev: true
|
||||
|
||||
/has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
dev: true
|
||||
|
||||
/regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
dev: true
|
||||
|
||||
/require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/rxjs@7.8.1:
|
||||
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
dev: true
|
||||
|
||||
/shell-quote@1.8.1:
|
||||
resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
|
||||
dev: true
|
||||
|
||||
/spawn-command@0.0.2:
|
||||
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
|
||||
dev: true
|
||||
|
||||
/string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
dev: true
|
||||
|
||||
/strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
dev: true
|
||||
|
||||
/supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
dev: true
|
||||
|
||||
/supports-color@8.1.1:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
dev: true
|
||||
|
||||
/tree-kill@1.2.2:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
dev: true
|
||||
|
||||
/wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
dev: true
|
||||
|
||||
/y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/yargs@17.7.2:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
escalade: 3.1.2
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
dev: true
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@upnd/upend",
|
||||
"version": "0.5.5",
|
||||
"version": "0.4.1",
|
||||
"description": "Client library to interact with the UpEnd system.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
@ -22,12 +22,6 @@ const dbg = debug("upend:api");
|
|||
|
||||
export type { AddressComponents };
|
||||
|
||||
export type UpendApiError = {
|
||||
kind: "Unauthorized" | "HttpError" | "FetchError" | "Unknown";
|
||||
message?: string;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
export class UpEndApi {
|
||||
private instanceUrl = "";
|
||||
private readonly wasmExtensions: UpEndWasmExtensions | undefined = undefined;
|
||||
|
@ -35,21 +29,15 @@ export class UpEndApi {
|
|||
|
||||
private queryOnceLRU = new LRU<string, UpListing>({ max: 128 });
|
||||
private inFlightRequests: { [key: string]: Promise<UpListing> | null } = {};
|
||||
private key: string | undefined;
|
||||
private readonly onError: ((error: UpendApiError) => void) | undefined;
|
||||
|
||||
constructor(config?: {
|
||||
constructor(config: {
|
||||
instanceUrl?: string;
|
||||
wasmExtensions?: UpEndWasmExtensions;
|
||||
timeout?: number;
|
||||
authKey?: string;
|
||||
onError?: (error: UpendApiError) => void;
|
||||
}) {
|
||||
this.setInstanceUrl(config?.instanceUrl || "http://localhost:8093");
|
||||
this.wasmExtensions = config?.wasmExtensions;
|
||||
this.timeout = config?.timeout || 30_000;
|
||||
this.key = config?.authKey;
|
||||
this.onError = config?.onError;
|
||||
this.setInstanceUrl(config.instanceUrl || "http://localhost:8093");
|
||||
this.wasmExtensions = config.wasmExtensions;
|
||||
this.timeout = config.timeout || 30_000;
|
||||
}
|
||||
|
||||
public setInstanceUrl(apiUrl: string) {
|
||||
|
@ -65,10 +53,10 @@ export class UpEndApi {
|
|||
options?: ApiFetchOptions,
|
||||
): Promise<UpObject> {
|
||||
dbg("Fetching Entity %s", address);
|
||||
const entityFetch = await this.fetch(
|
||||
`${this.apiUrl}/obj/${address}`,
|
||||
options,
|
||||
);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const entityFetch = await fetch(`${this.apiUrl}/obj/${address}`, {
|
||||
signal,
|
||||
});
|
||||
const entityResult = (await entityFetch.json()) as EntityListing;
|
||||
const entityListing = new UpListing(entityResult.entries);
|
||||
return entityListing.getObject(address);
|
||||
|
@ -76,7 +64,8 @@ export class UpEndApi {
|
|||
|
||||
public async fetchEntry(address: string, options?: ApiFetchOptions) {
|
||||
dbg("Fetching entry %s", address);
|
||||
const response = await this.fetch(`${this.apiUrl}/raw/${address}`, options);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/raw/${address}`, { signal });
|
||||
const data = await response.json();
|
||||
const listing = new UpListing({ address: data });
|
||||
return listing.entries[0];
|
||||
|
@ -93,10 +82,12 @@ export class UpEndApi {
|
|||
if (!this.inFlightRequests[queryStr]) {
|
||||
dbg(`Querying: ${query}`);
|
||||
this.inFlightRequests[queryStr] = new Promise((resolve, reject) => {
|
||||
this.fetch(`${this.apiUrl}/query`, options, {
|
||||
const signal = this.getAbortSignal(options);
|
||||
fetch(`${this.apiUrl}/query`, {
|
||||
method: "POST",
|
||||
body: queryStr,
|
||||
keepalive: true,
|
||||
signal,
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
|
@ -126,10 +117,12 @@ export class UpEndApi {
|
|||
options?: ApiFetchOptions,
|
||||
): Promise<PutResult> {
|
||||
dbg("Putting %O", input);
|
||||
const response = await this.fetch(`${this.apiUrl}/obj`, options, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/obj`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(input),
|
||||
signal,
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
|
@ -148,10 +141,12 @@ export class UpEndApi {
|
|||
url += `?provenance=${provenance}`;
|
||||
}
|
||||
|
||||
const response = await this.fetch(url, options, {
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(value),
|
||||
signal,
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
|
@ -208,9 +203,10 @@ export class UpEndApi {
|
|||
xhr.send(formData);
|
||||
});
|
||||
} else {
|
||||
const response = await this.fetch(`${this.apiUrl}/blob`, options, {
|
||||
const response = await fetch(`${this.apiUrl}/blob`, {
|
||||
method: "PUT",
|
||||
body: formData,
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
@ -226,20 +222,12 @@ export class UpEndApi {
|
|||
options?: ApiFetchOptions,
|
||||
): Promise<void> {
|
||||
dbg("Deleting entry %s", address);
|
||||
await this.fetch(`${this.apiUrl}/obj/${address}`, options, {
|
||||
method: "DELETE",
|
||||
});
|
||||
const signal = this.getAbortSignal(options);
|
||||
await fetch(`${this.apiUrl}/obj/${address}`, { method: "DELETE", signal });
|
||||
}
|
||||
|
||||
public getRaw(
|
||||
address: Address,
|
||||
config?: { preview?: boolean; authenticated?: boolean },
|
||||
) {
|
||||
let result = `${this.apiUrl}/${config?.preview ? "thumb" : "raw"}/${address}`;
|
||||
if (config?.authenticated) {
|
||||
result += `?auth_key=${this.key}`;
|
||||
}
|
||||
return result;
|
||||
public getRaw(address: Address, preview = false) {
|
||||
return `${this.apiUrl}/${preview ? "thumb" : "raw"}/${address}`;
|
||||
}
|
||||
|
||||
public async fetchRaw(
|
||||
|
@ -248,24 +236,26 @@ export class UpEndApi {
|
|||
options?: ApiFetchOptions,
|
||||
) {
|
||||
dbg("Getting %s raw (preview = %s)", address, preview);
|
||||
return await this.fetch(this.getRaw(address, { preview }), options);
|
||||
const signal = this.getAbortSignal(options);
|
||||
return await fetch(this.getRaw(address, preview), { signal });
|
||||
}
|
||||
|
||||
public async refreshVault(options?: ApiFetchOptions) {
|
||||
dbg("Triggering vault refresh");
|
||||
return await this.fetch(`${this.apiUrl}/refresh`, options, {
|
||||
method: "POST",
|
||||
});
|
||||
const signal = this.getAbortSignal(options);
|
||||
return await fetch(`${this.apiUrl}/refresh`, { method: "POST", signal });
|
||||
}
|
||||
|
||||
public async nativeOpen(address: Address, options?: ApiFetchOptions) {
|
||||
dbg("Opening %s natively", address);
|
||||
return this.fetch(`${this.apiUrl}/raw/${address}?native=1`, options);
|
||||
const signal = this.getAbortSignal(options);
|
||||
return fetch(`${this.apiUrl}/raw/${address}?native=1`, { signal });
|
||||
}
|
||||
|
||||
public async fetchRoots(options?: ApiFetchOptions): Promise<ListingResult> {
|
||||
dbg("Fetching hierarchical roots...");
|
||||
const response = await this.fetch(`${this.apiUrl}/hier_roots`, options);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/hier_roots`, { signal });
|
||||
const roots = await response.json();
|
||||
dbg("Hierarchical roots: %O", roots);
|
||||
return roots;
|
||||
|
@ -273,7 +263,8 @@ export class UpEndApi {
|
|||
|
||||
public async fetchJobs(options?: ApiFetchOptions): Promise<IJob[]> {
|
||||
// dbg("Fetching jobs...");
|
||||
const response = await this.fetch(`${this.apiUrl}/jobs`, options);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/jobs`, { signal });
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
|
@ -281,7 +272,8 @@ export class UpEndApi {
|
|||
options?: ApiFetchOptions,
|
||||
): Promise<AttributeListingResult> {
|
||||
dbg("Fetching all attributes...");
|
||||
const response = await this.fetch(`${this.apiUrl}/all/attributes`, options);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/all/attributes`, { signal });
|
||||
const result = await response.json();
|
||||
dbg("All attributes: %O", result);
|
||||
return await result;
|
||||
|
@ -289,25 +281,19 @@ export class UpEndApi {
|
|||
|
||||
public async fetchInfo(options?: ApiFetchOptions): Promise<VaultInfo> {
|
||||
dbg("Fetching vault info...");
|
||||
const response = await this.fetch(`${this.apiUrl}/info`, options);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/info`, { signal });
|
||||
const result = await response.json();
|
||||
dbg("Vault info: %O", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async fetchOptions(options?: ApiFetchOptions): Promise<VaultOptions> {
|
||||
dbg("Fetching vault options...");
|
||||
const response = await this.fetch(`${this.apiUrl}/options`, options);
|
||||
const result = await response.json();
|
||||
dbg("Vault options: %O", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async fetchStoreInfo(
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<{ [key: string]: StoreInfo }> {
|
||||
dbg("Fetching store info...");
|
||||
const response = await this.fetch(`${this.apiUrl}/stats/store`, options);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/stats/store`, { signal });
|
||||
const result = await response.json();
|
||||
dbg("Store info: %O");
|
||||
return await result;
|
||||
|
@ -323,15 +309,16 @@ export class UpEndApi {
|
|||
await this.wasmExtensions.init();
|
||||
return this.wasmExtensions.AddressTypeConstants[input];
|
||||
}
|
||||
response = await this.fetch(
|
||||
`${this.apiUrl}/address?type=${input}`,
|
||||
options,
|
||||
);
|
||||
const signal = this.getAbortSignal(options);
|
||||
response = await fetch(`${this.apiUrl}/address?type=${input}`, {
|
||||
signal,
|
||||
});
|
||||
} else {
|
||||
if ("urlContent" in input) {
|
||||
response = await this.fetch(
|
||||
const signal = this.getAbortSignal(options);
|
||||
response = await fetch(
|
||||
`${this.apiUrl}/address?url_content=${input.urlContent}`,
|
||||
options,
|
||||
{ signal },
|
||||
);
|
||||
} else {
|
||||
throw new Error("Input cannot be empty.");
|
||||
|
@ -365,7 +352,8 @@ export class UpEndApi {
|
|||
public async getVaultOptions(
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<VaultOptions> {
|
||||
const response = await this.fetch(`${this.apiUrl}/options`, options);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/options`, { signal });
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
|
@ -381,10 +369,12 @@ export class UpEndApi {
|
|||
payload["blob_mode"] = blob_mode;
|
||||
}
|
||||
|
||||
const response = await this.fetch(`${this.apiUrl}/options`, apiOptions, {
|
||||
const signal = this.getAbortSignal(apiOptions);
|
||||
const response = await fetch(`${this.apiUrl}/options`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
@ -392,145 +382,14 @@ export class UpEndApi {
|
|||
}
|
||||
}
|
||||
|
||||
public async authenticate(
|
||||
credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
},
|
||||
mode: "key",
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<{ key: string }>;
|
||||
public async authenticate(
|
||||
credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
},
|
||||
mode?: "cookie",
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<void>;
|
||||
public async authenticate(
|
||||
credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
},
|
||||
mode: "key" | "cookie" | undefined,
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<{ key: string } | void> {
|
||||
const via = mode || "cookie";
|
||||
const response = await this.fetch(
|
||||
`${this.apiUrl}/auth/login?via=${via}`,
|
||||
options,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(credentials),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw Error(await response.text());
|
||||
}
|
||||
|
||||
if (mode === "key") {
|
||||
const data = await response.json();
|
||||
if (!data.key) {
|
||||
throw Error("No key returned from server.");
|
||||
}
|
||||
this.key = data.key;
|
||||
return data.key;
|
||||
}
|
||||
}
|
||||
|
||||
public async register(credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
}): Promise<void> {
|
||||
await this.fetch(`${this.apiUrl}/auth/register`, undefined, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
}
|
||||
|
||||
public async authStatus(
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<{ user: string } | undefined> {
|
||||
const response = await this.fetch(`${this.apiUrl}/auth/whoami`, options);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
public async resetAuth(mode: "key"): Promise<void>;
|
||||
public async resetAuth(
|
||||
mode?: "cookie",
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<void>;
|
||||
public async resetAuth(
|
||||
mode?: "key" | "cookie",
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<void> {
|
||||
if (mode === "key") {
|
||||
this.key = undefined;
|
||||
} else {
|
||||
await this.fetch(`${this.apiUrl}/auth/logout`, options, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getAbortSignal(options: ApiFetchOptions | undefined) {
|
||||
const controller = options?.abortController || new AbortController();
|
||||
const timeout = options?.timeout || this.timeout;
|
||||
if (timeout > 0) {
|
||||
setTimeout(() => {
|
||||
dbg("Aborting request after %d ms", timeout);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
setTimeout(() => controller.abort(), timeout);
|
||||
}
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
private async fetch(
|
||||
url: string,
|
||||
options: ApiFetchOptions | undefined,
|
||||
requestInit?: RequestInit & { headers?: Record<string, string> },
|
||||
): Promise<Response> {
|
||||
const signal = this.getAbortSignal(options);
|
||||
const headers = requestInit?.headers || {};
|
||||
if (this.key) {
|
||||
headers["Authorization"] = `Bearer ${this.key}`;
|
||||
}
|
||||
|
||||
let result: Response;
|
||||
let error: UpendApiError | undefined;
|
||||
try {
|
||||
result = await fetch(url, {
|
||||
...requestInit,
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
if (!result.ok) {
|
||||
if (result.status === 401) {
|
||||
error = { kind: "Unauthorized", message: await result.text() };
|
||||
} else {
|
||||
error = {
|
||||
kind: "HttpError",
|
||||
message: `HTTP Error ${result.status}: ${result.statusText}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error = { kind: "FetchError", error: e as Error };
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (this.onError) {
|
||||
this.onError(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result!;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiFetchOptions {
|
||||
|
@ -539,7 +398,6 @@ export interface ApiFetchOptions {
|
|||
}
|
||||
|
||||
export type VaultBlobMode = "Flat" | "Mirror" | "Incoming";
|
||||
|
||||
export interface VaultOptions {
|
||||
blob_mode: VaultBlobMode;
|
||||
}
|
||||
|
|
|
@ -121,7 +121,6 @@ export class UpEntry extends UpObject implements IEntry {
|
|||
attribute: string;
|
||||
value: IValue;
|
||||
provenance: string;
|
||||
user: string;
|
||||
timestamp: string;
|
||||
|
||||
constructor(address: string, entry: IEntry, listing: UpListing) {
|
||||
|
@ -131,7 +130,6 @@ export class UpEntry extends UpObject implements IEntry {
|
|||
this.attribute = entry.attribute;
|
||||
this.value = entry.value;
|
||||
this.provenance = entry.provenance;
|
||||
this.user = entry.user;
|
||||
this.timestamp = entry.timestamp;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,8 +18,6 @@ export interface IEntry {
|
|||
value: IValue;
|
||||
/** The origin or provenance of the data entry (e.g. SYSTEM or USER API...) */
|
||||
provenance: string;
|
||||
/** The user who created the data entry. */
|
||||
user: string;
|
||||
/** The timestamp when the data entry was created in RFC 3339 format. */
|
||||
timestamp: string;
|
||||
}
|
||||
|
@ -99,7 +97,6 @@ export interface VaultInfo {
|
|||
location: string;
|
||||
version: string;
|
||||
desktop: boolean;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
export interface StoreInfo {
|
||||
|
|
|
@ -10,6 +10,3 @@ vite.config.js.timestamp-*
|
|||
vite.config.ts.timestamp-*
|
||||
|
||||
/static/vendor
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
|
|
@ -35,6 +35,8 @@
|
|||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook": "^7.6.16",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
|
@ -45,7 +47,6 @@
|
|||
"dependencies": {
|
||||
"@ibm/plex": "^6.3.0",
|
||||
"@recogito/annotorious": "^2.7.11",
|
||||
"@sentry/sveltekit": "^7.109.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/debug": "^4.1.12",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,23 +0,0 @@
|
|||
import * as Sentry from '@sentry/sveltekit';
|
||||
import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit';
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://9b966481f93abec5d80f1d48dd1afec1@o4506614333308928.ingest.us.sentry.io/4506614334423040',
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// If the entire session is not sampled, use the below sample rate to sample
|
||||
// sessions when an error occurs.
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
// If you don't want to use Session Replay, just remove the line below:
|
||||
integrations: [replayIntegration(), Sentry.feedbackIntegration({ colorScheme: 'dark' })],
|
||||
|
||||
enabled: process.env.NODE_ENV !== 'development'
|
||||
});
|
||||
|
||||
// If you have a custom error handler, pass it to `handleErrorWithSentry`
|
||||
export const handleError = handleErrorWithSentry();
|
|
@ -1,17 +0,0 @@
|
|||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit';
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://9b966481f93abec5d80f1d48dd1afec1@o4506614333308928.ingest.us.sentry.io/4506614334423040',
|
||||
tracesSampleRate: 1.0
|
||||
|
||||
// uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||
// spotlight: import.meta.env.DEV,
|
||||
});
|
||||
|
||||
// If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function.
|
||||
export const handle = sequence(sentryHandle());
|
||||
|
||||
// If you have a custom error handler, pass it to `handleErrorWithSentry`
|
||||
export const handleError = handleErrorWithSentry();
|
|
@ -1,29 +1,6 @@
|
|||
import { UpEndApi } from '@upnd/upend';
|
||||
import { UpEndWasmExtensionsWeb } from '@upnd/upend/wasm/web';
|
||||
import wasmURL from '@upnd/wasm-web/upend_wasm_bg.wasm?url';
|
||||
import { type StartStopNotifier, writable, type Writable } from 'svelte/store';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
|
||||
const wasm = new UpEndWasmExtensionsWeb(wasmURL);
|
||||
const api = new UpEndApi({ instanceUrl: '/', wasmExtensions: wasm });
|
||||
export default api;
|
||||
|
||||
export const currentUser: Writable<string | undefined> = writable(
|
||||
undefined as string | undefined,
|
||||
((set) => {
|
||||
api.authStatus().then((result) => {
|
||||
set(result?.user);
|
||||
Sentry.setUser({ id: result?.user });
|
||||
});
|
||||
}) as StartStopNotifier<string | undefined>
|
||||
);
|
||||
|
||||
export async function login(credentials: { username: string; password: string }) {
|
||||
await api.authenticate(credentials);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
await api.resetAuth();
|
||||
window.location.reload();
|
||||
}
|
||||
export default new UpEndApi({ instanceUrl: '/', wasmExtensions: wasm });
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<script context="module" lang="ts">
|
||||
import mitt from 'mitt';
|
||||
import type { Address } from '@upnd/upend/types';
|
||||
|
||||
export type AddEvents = {
|
||||
choose: void;
|
||||
files: File[];
|
||||
urls: string[];
|
||||
destination: Address;
|
||||
};
|
||||
export const addEmitter = mitt<AddEvents>();
|
||||
</script>
|
||||
|
@ -19,23 +17,15 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { selected } from '$lib/components/EntitySelect.svelte';
|
||||
import Modal from '$lib/components/layout/Modal.svelte';
|
||||
import Selector, { type SelectorValue } from '$lib/components/utils/Selector.svelte';
|
||||
import { ATTR_IN } from '@upnd/upend/constants';
|
||||
|
||||
let files: File[] = [];
|
||||
let URLs: string[] = [];
|
||||
let uploading = false;
|
||||
let abortController: AbortController | undefined;
|
||||
|
||||
let destination: Address | undefined;
|
||||
|
||||
let progress: Record<string, number> = {};
|
||||
let totalProgress: number | undefined;
|
||||
|
||||
let filesElement: HTMLDivElement;
|
||||
|
||||
$: visible = files.length + URLs.length > 0 || destination;
|
||||
$: visible = files.length + URLs.length > 0;
|
||||
|
||||
addEmitter.on('files', (ev) => {
|
||||
ev.forEach((file) => {
|
||||
|
@ -46,52 +36,20 @@
|
|||
});
|
||||
});
|
||||
|
||||
addEmitter.on('destination', (ev) => {
|
||||
destination = ev;
|
||||
});
|
||||
|
||||
function onDestinationSelected(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
if (ev.detail?.t === 'Address') {
|
||||
destination = ev.detail.c;
|
||||
} else {
|
||||
destination = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function upload() {
|
||||
uploading = true;
|
||||
|
||||
try {
|
||||
abortController = new AbortController();
|
||||
const addresses: string[] = [];
|
||||
for (const [idx, file] of files.entries()) {
|
||||
filesElement
|
||||
?.querySelectorAll('.entry')
|
||||
[idx]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
for (const file of files) {
|
||||
const address = await api.putBlob(file, {
|
||||
abortController,
|
||||
onProgress: (p) => {
|
||||
progress[file.name] = (p.loaded / p.total) * 100;
|
||||
totalProgress = Object.values(progress).reduce((a, b) => a + b, 0) / files.length;
|
||||
},
|
||||
timeout: -1
|
||||
});
|
||||
if (destination) {
|
||||
await api.putEntry({
|
||||
entity: address,
|
||||
attribute: ATTR_IN,
|
||||
value: {
|
||||
t: 'Address',
|
||||
c: destination
|
||||
}
|
||||
});
|
||||
}
|
||||
addresses.push(address);
|
||||
|
||||
if (!uploading) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (addresses.length == 1) {
|
||||
|
@ -109,17 +67,11 @@
|
|||
}
|
||||
|
||||
function reset() {
|
||||
if (uploading) {
|
||||
const msg = $i18n.t('Are you sure you want to cancel the upload?');
|
||||
if (!confirm(msg)) return;
|
||||
if (!uploading) {
|
||||
files = [];
|
||||
URLs = [];
|
||||
progress = {};
|
||||
}
|
||||
abortController?.abort();
|
||||
|
||||
files = [];
|
||||
URLs = [];
|
||||
progress = {};
|
||||
uploading = false;
|
||||
destination = undefined;
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
|
@ -132,21 +84,14 @@
|
|||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
function onBeforeUnload(ev: BeforeUnloadEvent) {
|
||||
if (files.length || uploading) {
|
||||
ev.preventDefault();
|
||||
ev.returnValue = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={onBeforeUnload} />
|
||||
<svelte:body on:keydown={onKeydown} />
|
||||
|
||||
{#if visible}
|
||||
<Modal on:close={reset}>
|
||||
<div class="files" bind:this={filesElement}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||
<div class="addmodal-container" class:visible class:uploading on:click={reset}>
|
||||
<div class="addmodal" on:click|stopPropagation>
|
||||
<div class="files">
|
||||
{#each files as file}
|
||||
<div class="entry">
|
||||
<div class="row">
|
||||
|
@ -183,28 +128,17 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="controls-destination">
|
||||
<div class="label"><Icon plain name="download" /> {$i18n.t('Destination')}</div>
|
||||
<Selector
|
||||
initial={destination ? { t: 'Address', c: destination } : undefined}
|
||||
types={['Address', 'NewAddress']}
|
||||
placeholder={$i18n.t('Choose automatically') || ''}
|
||||
on:input={onDestinationSelected}
|
||||
/>
|
||||
</div>
|
||||
<div class="controls-submit">
|
||||
<IconButton small disabled={uploading} name="upload" on:click={upload}>
|
||||
{$i18n.t('Upload')}
|
||||
</IconButton>
|
||||
</div>
|
||||
<IconButton small disabled={uploading} name="upload" on:click={upload}>
|
||||
{$i18n.t('Upload')}
|
||||
</IconButton>
|
||||
</div>
|
||||
{#if uploading}
|
||||
<div class="progress">
|
||||
<ProgressBar value={totalProgress} />
|
||||
</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.addmodal-container {
|
||||
|
@ -229,8 +163,6 @@
|
|||
filter: brightness(0.85);
|
||||
}
|
||||
}
|
||||
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.addmodal {
|
||||
|
@ -300,23 +232,8 @@
|
|||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 3em;
|
||||
margin-top: 0.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.controls-destination {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
font-size: 1rem;
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
.controls-submit {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.progress {
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
export let address: string | undefined = undefined;
|
||||
export let index: number;
|
||||
export let only: boolean;
|
||||
export let background: string | undefined = undefined;
|
||||
export let forceDetail = false;
|
||||
let shifted = false;
|
||||
let key = Math.random();
|
||||
|
@ -63,13 +64,35 @@
|
|||
window.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
let resultBackground = background;
|
||||
let imageBackground: string | undefined = undefined;
|
||||
$: {
|
||||
if (background?.startsWith('url(')) {
|
||||
imageBackground = background;
|
||||
resultBackground = 'transparent';
|
||||
} else {
|
||||
resultBackground = background;
|
||||
imageBackground = undefined;
|
||||
}
|
||||
resultBackground ||= 'var(--background-lighter)';
|
||||
}
|
||||
|
||||
function reload() {
|
||||
key = Math.random();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="browse-column" class:detail on:mousemove={(ev) => (shifted = ev.shiftKey)}>
|
||||
<div
|
||||
class="browse-column"
|
||||
class:detail
|
||||
style="--background: {resultBackground}"
|
||||
class:image-background={Boolean(imageBackground)}
|
||||
on:mousemove={(ev) => (shifted = ev.shiftKey)}
|
||||
>
|
||||
{#if imageBackground}
|
||||
<div class="background" style="background-image: {imageBackground}" />
|
||||
{/if}
|
||||
<div class="view" style="--width: {width}px">
|
||||
<header>
|
||||
{#if address}
|
||||
|
@ -142,7 +165,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background: var(--background-lighter);
|
||||
background: var(--background);
|
||||
color: var(--foreground-lighter);
|
||||
border: 1px solid var(--foreground-lightest);
|
||||
border-radius: 0.5em;
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import LabelBorder from './utils/LabelBorder.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { type Address } from '@upnd/upend/types';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
const dispatch = createEventDispatcher<{
|
||||
highlighted: string | undefined;
|
||||
add: Address;
|
||||
|
@ -17,7 +16,6 @@
|
|||
export let hide = false;
|
||||
|
||||
export let header = '';
|
||||
export let icon: string | null = null;
|
||||
export let confirmRemoveMessage: string | null = $i18n.t('Are you sure you want to remove this?');
|
||||
export let emptyMessage = $i18n.t('Nothing to show.');
|
||||
|
||||
|
@ -26,8 +24,8 @@
|
|||
|
||||
$: if (adding && selector) selector.focus();
|
||||
|
||||
async function add(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
if (ev.detail?.t !== 'Address') {
|
||||
async function add(ev: CustomEvent<SelectorValue>) {
|
||||
if (ev.detail.t !== 'Address') {
|
||||
return;
|
||||
}
|
||||
dispatch('add', ev.detail.c);
|
||||
|
@ -41,10 +39,7 @@
|
|||
</script>
|
||||
|
||||
<LabelBorder {hide}>
|
||||
<span slot="header"
|
||||
>{#if icon}<Icon plain name={icon} />
|
||||
{/if}{header}</span
|
||||
>
|
||||
<span slot="header">{header}</span>
|
||||
|
||||
{#if adding}
|
||||
<div class="selector">
|
||||
|
|
|
@ -90,14 +90,14 @@
|
|||
{#if group}
|
||||
{#if icon}
|
||||
<div class="icon">
|
||||
<Icon plain name={icon} />
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
<UpObject link address={group} labels={title ? [title] : undefined} />
|
||||
{:else}
|
||||
{#if icon}
|
||||
<div class="icon">
|
||||
<Icon plain name={icon} />
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
{title || ''}
|
||||
|
|
|
@ -22,12 +22,12 @@
|
|||
import { debug } from 'debug';
|
||||
import { Any } from '@upnd/upend/query';
|
||||
import { isDefined } from '$lib/util/werk';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
const dbg = debug('kestrel:Inspect');
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
resolved: string[];
|
||||
close: void;
|
||||
background: string | undefined;
|
||||
}>();
|
||||
|
||||
export let address: string;
|
||||
|
@ -193,7 +193,7 @@
|
|||
const allAttributes = (
|
||||
await Promise.all(
|
||||
($entity?.attr[`~${ATTR_OF}`] ?? []).map(async (e) => {
|
||||
return { address: e.address, components: await api.addressToComponents(e.entity) };
|
||||
return { address: e.entity, components: await api.addressToComponents(e.entity) };
|
||||
})
|
||||
)
|
||||
)
|
||||
|
@ -393,6 +393,19 @@
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
$: {
|
||||
const cover = $entity?.attr['COVER']?.[0];
|
||||
if (!cover) {
|
||||
dispatch('background', undefined);
|
||||
} else {
|
||||
switch (cover.value.t) {
|
||||
case 'Address':
|
||||
dispatch('background', `url('${api.getRaw(cover.value.c)}')`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -444,7 +457,6 @@
|
|||
{#if currentUntypedProperties.length > 0}
|
||||
<EntryView
|
||||
title={$i18n.t('Other Properties') || ''}
|
||||
icon="shape-triangle"
|
||||
widgets={attributeWidgets}
|
||||
entries={currentUntypedProperties}
|
||||
on:change={onChange}
|
||||
|
@ -455,7 +467,6 @@
|
|||
{#if currentUntypedLinks.length > 0}
|
||||
<EntryView
|
||||
title={$i18n.t('Links') || ''}
|
||||
icon="right-arrow-circle"
|
||||
widgets={linkWidgets}
|
||||
entries={currentUntypedLinks}
|
||||
on:change={onChange}
|
||||
|
@ -466,7 +477,6 @@
|
|||
{#if !correctlyTagged || !incorrectlyTagged}
|
||||
<EntryView
|
||||
title={`${$i18n.t('Members')}`}
|
||||
icon="link"
|
||||
widgets={taggedWidgets}
|
||||
entries={tagged}
|
||||
on:change={onChange}
|
||||
|
@ -475,7 +485,6 @@
|
|||
{:else}
|
||||
<EntryView
|
||||
title={`${$i18n.t('Typed Members')} (${correctlyTagged.length})`}
|
||||
icon="link"
|
||||
widgets={taggedWidgets}
|
||||
entries={tagged.filter((e) => correctlyTagged?.includes(e.entity))}
|
||||
on:change={onChange}
|
||||
|
@ -483,7 +492,6 @@
|
|||
/>
|
||||
<EntryView
|
||||
title={`${$i18n.t('Untyped members')} (${incorrectlyTagged.length})`}
|
||||
icon="unlink"
|
||||
widgets={taggedWidgets}
|
||||
entries={tagged.filter((e) => incorrectlyTagged?.includes(e.entity))}
|
||||
on:change={onChange}
|
||||
|
@ -494,7 +502,6 @@
|
|||
{#if currentBacklinks.length > 0}
|
||||
<EntryView
|
||||
title={`${$i18n.t('Referred to')} (${currentBacklinks.length})`}
|
||||
icon="left-arrow-circle"
|
||||
entries={currentBacklinks}
|
||||
on:change={onChange}
|
||||
{address}
|
||||
|
@ -503,10 +510,7 @@
|
|||
|
||||
{#if $entityInfo?.t === 'Attribute'}
|
||||
<LabelBorder>
|
||||
<span slot="header">
|
||||
<Icon plain name="color" />
|
||||
{$i18n.t('Used')} ({attributesUsed.length})
|
||||
</span>
|
||||
<span slot="header">{$i18n.t('Used')} ({attributesUsed.length})</span>
|
||||
<EntryList columns="entity,value" entries={attributesUsed} orderByValue />
|
||||
</LabelBorder>
|
||||
{/if}
|
||||
|
@ -523,13 +527,13 @@
|
|||
<h2>{$i18n.t('Attributes')}</h2>
|
||||
<EntryList
|
||||
entries={$entity?.attributes || []}
|
||||
columns={detail ? 'timestamp, user, provenance, attribute, value' : 'attribute, value'}
|
||||
columns={detail ? 'timestamp, provenance, attribute, value' : 'attribute, value'}
|
||||
on:change={onChange}
|
||||
/>
|
||||
<h2>{$i18n.t('Backlinks')}</h2>
|
||||
<EntryList
|
||||
entries={$entity?.backlinks || []}
|
||||
columns={detail ? 'timestamp, user, provenance, entity, attribute' : 'entity, attribute'}
|
||||
columns={detail ? 'timestamp, provenance, entity, attribute' : 'entity, attribute'}
|
||||
on:change={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
<EntitySetEditor
|
||||
entities={Object.keys(groups)}
|
||||
header={$i18n.t('Groups') || ''}
|
||||
icon="link-alt"
|
||||
hide={Object.keys(groups).length === 0}
|
||||
on:add={(e) => addGroup(e.detail)}
|
||||
on:remove={(e) => removeGroup(e.detail)}
|
||||
|
|
|
@ -10,8 +10,6 @@
|
|||
import { ATTR_OF } from '@upnd/upend/constants';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import LabelBorder from './utils/LabelBorder.svelte';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let entity: Readable<UpObject | undefined>;
|
||||
|
@ -23,7 +21,6 @@
|
|||
|
||||
let types: Array<{ address: string; entry: UpEntry; required: UpEntry | undefined }> = [];
|
||||
$: updateTypes($entity?.attr[`~${ATTR_OF}`] || []);
|
||||
|
||||
async function updateTypes(entries: UpEntry[]) {
|
||||
types = [];
|
||||
const query = await api.query(
|
||||
|
@ -50,8 +47,8 @@
|
|||
});
|
||||
}
|
||||
|
||||
async function add(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
if (!$entity || ev.detail?.t !== 'Attribute') {
|
||||
async function add(ev: CustomEvent<SelectorValue>) {
|
||||
if (!$entity || ev.detail.t !== 'Attribute') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -101,7 +98,7 @@
|
|||
|
||||
{#if types.length || $entity?.attr['~IN']?.length}
|
||||
<LabelBorder hide={types.length === 0}>
|
||||
<span slot="header"><Icon plain name="list-check" /> {$i18n.t('Type Attributes')}</span>
|
||||
<span slot="header">{$i18n.t('Type Attributes')}</span>
|
||||
{#if adding}
|
||||
<div class="selector">
|
||||
<Selector
|
||||
|
@ -125,11 +122,7 @@
|
|||
class:required={type.required}
|
||||
on:click={() => setRequired(type.entry, !type.required)}
|
||||
>
|
||||
{#if type.required}
|
||||
<Icon plain name="lock" /> {$i18n.t('Required')}
|
||||
{:else}
|
||||
<Icon plain name="lock-open" /> {$i18n.t('Optional')}
|
||||
{/if}
|
||||
{type.required ? $i18n.t('Required') : $i18n.t('Optional')}
|
||||
</button>
|
||||
<div class="controls">
|
||||
<IconButton name="x-circle" on:click={() => remove(type.entry)} />
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { i18n } from '$lib/i18n';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
import { login } from '$lib/api';
|
||||
import Modal from '$lib/components/layout/Modal.svelte';
|
||||
import { type UpendApiError } from '@upnd/upend/api';
|
||||
|
||||
let username = '';
|
||||
let password = '';
|
||||
let error: string | undefined;
|
||||
let authenticating = false;
|
||||
|
||||
async function submit() {
|
||||
error = undefined;
|
||||
try {
|
||||
authenticating = true;
|
||||
await login({ username, password });
|
||||
} catch (e) {
|
||||
error = (e as UpendApiError).message || (e as UpendApiError).kind;
|
||||
} finally {
|
||||
authenticating = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal disabled={authenticating}>
|
||||
<h2>
|
||||
<Icon name="lock" />
|
||||
{$i18n.t('Authorization required')}
|
||||
</h2>
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<input
|
||||
name="username"
|
||||
placeholder={$i18n.t('Username')}
|
||||
type="text"
|
||||
bind:value={username}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
name="password"
|
||||
placeholder={$i18n.t('Password')}
|
||||
type="password"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
<button type="submit"> <Icon plain name="log-in" /> {$i18n.t('Login')}</button>
|
||||
</form>
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$lib/styles/colors';
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: colors.$red;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -229,9 +229,9 @@
|
|||
resizeObserver.observe(viewEl as any);
|
||||
});
|
||||
|
||||
async function onSelectorInput(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
async function onSelectorInput(ev: CustomEvent<SelectorValue>) {
|
||||
const value = ev.detail;
|
||||
if (value?.t !== 'Address') return;
|
||||
if (value.t !== 'Address') return;
|
||||
const address = value.c;
|
||||
|
||||
const [xValue, yValue] = selectorCoords as any;
|
||||
|
@ -261,7 +261,7 @@
|
|||
types={['Attribute', 'NewAttribute']}
|
||||
initial={x ? { t: 'Attribute', name: x } : undefined}
|
||||
on:input={(ev) => {
|
||||
if (ev.detail?.t === 'Attribute') x = ev.detail.name;
|
||||
if (ev.detail.t === 'Attribute') x = ev.detail.name;
|
||||
}}
|
||||
/>
|
||||
<div class="value">
|
||||
|
@ -277,7 +277,7 @@
|
|||
types={['Attribute', 'NewAttribute']}
|
||||
initial={y ? { t: 'Attribute', name: y } : undefined}
|
||||
on:input={(ev) => {
|
||||
if (ev.detail?.t === 'Attribute') y = ev.detail.name;
|
||||
if (ev.detail.t === 'Attribute') y = ev.detail.name;
|
||||
}}
|
||||
/>
|
||||
<div class="value">
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<script lang="ts">
|
||||
import { attributeLabels } from '$lib/util/labels';
|
||||
import UpLink from '$lib/components/display/UpLink.svelte';
|
||||
import Ellipsis from '$lib/components/utils/Ellipsis.svelte';
|
||||
|
||||
export let attribute: string;
|
||||
export let mark = false;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="attribute mark-attribute"
|
||||
class:formatted={Boolean(Object.keys($attributeLabels).includes(attribute))}
|
||||
class:mark
|
||||
>
|
||||
<UpLink to={{ attribute }}>
|
||||
<Ellipsis
|
||||
value={$attributeLabels[attribute] || attribute}
|
||||
title={$attributeLabels[attribute]
|
||||
? `${$attributeLabels[attribute]} (${attribute})`
|
||||
: attribute}
|
||||
/>
|
||||
</UpLink>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.attribute {
|
||||
min-width: 0;
|
||||
font-family: var(--monospace-font);
|
||||
&.formatted {
|
||||
font-family: var(--default-font);
|
||||
}
|
||||
|
||||
&.mark {
|
||||
position: relative;
|
||||
top: 0.2em;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 0.9em;
|
||||
&::after {
|
||||
content: '\00a0→';
|
||||
|
||||
position: absolute;
|
||||
top: calc(-50% + 2px);
|
||||
left: calc(50% - 2px);
|
||||
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.66em;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type { UpEntry } from '@upnd/upend';
|
||||
import { attributeLabels } from '$lib/util/labels';
|
||||
import UpObject from './UpObject.svelte';
|
||||
import Ellipsis from '$lib/components/utils/Ellipsis.svelte';
|
||||
import UpAttribute from '$lib/components/display/UpAttribute.svelte';
|
||||
export let resolve = true;
|
||||
|
||||
export let entry: UpEntry;
|
||||
|
@ -12,14 +12,12 @@
|
|||
<div class="entity">
|
||||
<UpObject plain link address={entry.entity} labels={resolve ? undefined : []} />
|
||||
</div>
|
||||
<div class="attribute" title={entry.attribute}>
|
||||
<Ellipsis value={$attributeLabels[entry.attribute] || entry.attribute} />
|
||||
</div>
|
||||
<UpAttribute attribute={entry.attribute} mark />
|
||||
<div class="value value-{entry.value.t.toLowerCase()}">
|
||||
{#if entry.value.t === 'Address'}
|
||||
<UpObject link address={entry.value.c} labels={resolve ? undefined : []} />
|
||||
{:else}
|
||||
<Ellipsis value={entry.value.c?.toString() || ''} />
|
||||
<Ellipsis value={entry.value.c} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,26 +39,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.attribute {
|
||||
position: relative;
|
||||
top: 0.2em;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 0.9em;
|
||||
&::after {
|
||||
content: '\00a0→';
|
||||
|
||||
position: absolute;
|
||||
top: calc(-50% + 2px);
|
||||
left: calc(50% - 2px);
|
||||
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.66em;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.value-value) {
|
||||
font-family: var(--monospace-font);
|
||||
}
|
||||
|
|
|
@ -152,22 +152,6 @@
|
|||
dispatch('change', { type: 'upsert', attribute: ATTR_LABEL, value: ev.detail });
|
||||
}
|
||||
}
|
||||
|
||||
let background: string | undefined;
|
||||
$: background = $entity?.get('COVER')?.toString();
|
||||
|
||||
let resultBackground = background;
|
||||
let imageBackground: string | undefined = undefined;
|
||||
$: {
|
||||
if (background) {
|
||||
imageBackground = `url(${api.getRaw(background)})`;
|
||||
resultBackground = 'transparent';
|
||||
} else {
|
||||
resultBackground = background;
|
||||
imageBackground = undefined;
|
||||
}
|
||||
resultBackground ||= 'var(--background-lighter)';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -176,8 +160,6 @@
|
|||
class:right-active={address == $addresses[$index + 1]}
|
||||
class:selected={select && $selected.includes(address)}
|
||||
class:plain
|
||||
style="--background: {resultBackground}"
|
||||
class:image-background={Boolean(imageBackground)}
|
||||
>
|
||||
<div
|
||||
class="address"
|
||||
|
@ -185,23 +167,26 @@
|
|||
class:banner
|
||||
class:show-type={$entityInfo?.t === 'Url' && !addressIds.length}
|
||||
>
|
||||
{#if imageBackground}
|
||||
<div class="image-gradient"></div>
|
||||
<div class="image-background" style="background-image: {imageBackground}"></div>
|
||||
{/if}
|
||||
<HashBadge {address} />
|
||||
<div class="label" class:resolving title={displayLabel}>
|
||||
{#if banner && !link}
|
||||
<Editable value={{ t: 'String', c: displayLabel }} types={['String']} on:edit={onLabelEdit}>
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
</Editable>
|
||||
{:else if link}
|
||||
<UpLink passthrough to={{ entity: address }}>
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
</UpLink>
|
||||
{:else}
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
{/if}
|
||||
<Editable
|
||||
value={{ t: 'String', c: displayLabel }}
|
||||
editable={banner}
|
||||
types={['String']}
|
||||
on:edit={onLabelEdit}
|
||||
>
|
||||
<div class="label-inner">
|
||||
{#if banner && hasFile}
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
{:else if link}
|
||||
<UpLink to={{ entity: address }}>
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
</UpLink>
|
||||
{:else}
|
||||
<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
|
||||
{/if}
|
||||
</div>
|
||||
</Editable>
|
||||
{#if $entity?.get(ATTR_KEY) && !$entity?.get(ATTR_KEY)?.toString()?.startsWith('TYPE_')}
|
||||
<div class="key">{$entity.get(ATTR_KEY)}</div>
|
||||
{/if}
|
||||
|
@ -279,7 +264,6 @@
|
|||
}
|
||||
|
||||
.address {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
|
@ -291,7 +275,7 @@
|
|||
font-family: var(--monospace-font);
|
||||
line-break: anywhere;
|
||||
|
||||
background: var(--background);
|
||||
background: var(--background-lighter);
|
||||
border: 0.1em solid var(--foreground-lighter);
|
||||
border-radius: 0.2em;
|
||||
|
||||
|
@ -307,17 +291,10 @@
|
|||
}
|
||||
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
margin-left: 0.25em;
|
||||
|
||||
:global(a) {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.label-inner {
|
||||
|
@ -351,36 +328,22 @@
|
|||
}
|
||||
|
||||
&:not(.banner) .key {
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.show-type .secondary,
|
||||
&.banner .secondary {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.image-gradient {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.66) 16%, var(--background) 66%);
|
||||
z-index: -1;
|
||||
}
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
.image-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -2;
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: brightness(0.8);
|
||||
:global(a) {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -393,7 +356,6 @@
|
|||
}
|
||||
|
||||
.link-button {
|
||||
padding: 0.25em;
|
||||
opacity: 0.66;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
<div class="label">
|
||||
<UpObject {address} {labels} {banner} {select} link on:resolved />
|
||||
<UpObject {address} {labels} {banner} {select} on:resolved />
|
||||
</div>
|
||||
</div>
|
||||
</UpLink>
|
||||
|
|
|
@ -24,8 +24,7 @@
|
|||
|
||||
<style lang="scss">
|
||||
.upobject-label {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.backpath {
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script lang="ts">
|
||||
import { formatDuration } from '$lib/util/fragments/time';
|
||||
import Ellipsis from '$lib/components/utils/Ellipsis.svelte';
|
||||
import Editable from '$lib/components/utils/Editable.svelte';
|
||||
import UpObject from '$lib/components/display/UpObject.svelte';
|
||||
import { IValue } from '@upnd/upend';
|
||||
import { formatRelative, fromUnixTime } from 'date-fns';
|
||||
import filesize from 'filesize';
|
||||
import { ATTR_ADDED } from '@upnd/upend/constants';
|
||||
|
||||
export let value: IValue;
|
||||
export let attribute: string | undefined;
|
||||
export let labels: string[] | undefined = undefined;
|
||||
|
||||
function formatValue(value: string | number | null, attribute: string | undefined): string {
|
||||
if (attribute) {
|
||||
try {
|
||||
switch (attribute) {
|
||||
case 'FILE_SIZE':
|
||||
return filesize(parseInt(String(value), 10), { base: 2 });
|
||||
case ATTR_ADDED:
|
||||
case 'LAST_VISITED':
|
||||
return formatRelative(fromUnixTime(parseInt(String(value), 10)), new Date());
|
||||
case 'NUM_VISITED':
|
||||
return `${value} times`;
|
||||
case 'MEDIA_DURATION':
|
||||
return formatDuration(parseInt(String(value), 10));
|
||||
}
|
||||
} catch {
|
||||
// noop.
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="cell value mark-value" data-address={value.t === 'Address' ? value.c : undefined}>
|
||||
<Editable {value} on:edit>
|
||||
{#if value.t === 'Address'}
|
||||
<UpObject link address={String(value.c)} {labels} on:resolved />
|
||||
{:else}
|
||||
<div class:formatted={Boolean(formatValue(value.c, attribute))}>
|
||||
<Ellipsis value={formatValue(value.c, attribute) || String(value.c)} />
|
||||
</div>
|
||||
{/if}
|
||||
</Editable>
|
||||
</div>
|
|
@ -2,7 +2,7 @@
|
|||
import Icon from '../utils/Icon.svelte';
|
||||
import Jobs from './Jobs.svelte';
|
||||
import Notifications from './Notifications.svelte';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { i18n } from '../../i18n';
|
||||
|
||||
let hidden = true;
|
||||
let activeJobs: number;
|
||||
|
@ -20,8 +20,6 @@
|
|||
on:keydown={(ev) => {
|
||||
if (['Space', 'Enter'].includes(ev.key)) hidden = !hidden;
|
||||
}}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="info">
|
||||
{#if activeJobs > 0}
|
||||
|
@ -44,7 +42,6 @@
|
|||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 9;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -7,11 +7,8 @@
|
|||
import { i18n } from '$lib/i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { vaultInfo } from '$lib/util/info';
|
||||
import HeaderUserDropdown from '$lib/components/layout/HeaderUserDropdown.svelte';
|
||||
|
||||
let selector: Selector;
|
||||
let userDropdown = false;
|
||||
|
||||
let lastSearched: SelectorValue[] = [];
|
||||
|
||||
|
@ -28,7 +25,7 @@
|
|||
lastSearched = lastSearched.slice(0, 10);
|
||||
}
|
||||
|
||||
async function onInput(event: CustomEvent<SelectorValue | undefined>) {
|
||||
async function onInput(event: CustomEvent<SelectorValue>) {
|
||||
const value = event.detail;
|
||||
if (!value) return;
|
||||
|
||||
|
@ -60,7 +57,6 @@
|
|||
}
|
||||
|
||||
let fileInput: HTMLInputElement;
|
||||
|
||||
function onFileChange() {
|
||||
if (fileInput.files?.length) {
|
||||
addEmitter.emit('files', Array.from(fileInput.files));
|
||||
|
@ -95,21 +91,13 @@
|
|||
<Icon name="search" slot="prefix" />
|
||||
</Selector>
|
||||
</div>
|
||||
<button on:click={() => addEmitter.emit('choose')}>
|
||||
<button class="button" on:click={() => addEmitter.emit('choose')}>
|
||||
<Icon name="upload" />
|
||||
<input type="file" multiple bind:this={fileInput} on:change={onFileChange} />
|
||||
</button>
|
||||
<button on:click={() => rescan()} title="Rescan vault">
|
||||
<button class="button" on:click={() => rescan()} title="Rescan vault">
|
||||
<Icon name="refresh" />
|
||||
</button>
|
||||
<button
|
||||
class="user"
|
||||
disabled={$vaultInfo?.public}
|
||||
on:click|stopPropagation={() => (userDropdown = true)}
|
||||
>
|
||||
<Icon name="user" />
|
||||
</button>
|
||||
<HeaderUserDropdown bind:open={userDropdown} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
<script lang="ts">
|
||||
import api, { currentUser, logout } from '$lib/api';
|
||||
import { type UpendApiError } from '@upnd/upend/api';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import Modal from '$lib/components/layout/Modal.svelte';
|
||||
|
||||
export let open = false;
|
||||
|
||||
let changePasswordModal = false;
|
||||
let currentPassword = '';
|
||||
let newPassword = '';
|
||||
let newPasswordConfirm = '';
|
||||
|
||||
async function changePassword() {
|
||||
try {
|
||||
const result = await api.authenticate(
|
||||
{ username: $currentUser!, password: currentPassword },
|
||||
'key'
|
||||
);
|
||||
if (result) {
|
||||
await api.register({ username: $currentUser!, password: newPassword });
|
||||
alert($i18n.t('Password changed successfully').toString());
|
||||
changePasswordModal = false;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
alert(
|
||||
$i18n.t('Error authenticating: {error}', { error: (e as UpendApiError).message }).toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:body
|
||||
on:click={() => {
|
||||
open = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions a11y-click-events-have-key-events -->
|
||||
<div class="user-dropdown" transition:slide on:click|stopPropagation>
|
||||
<div class="user">
|
||||
<Icon plain name="user" />
|
||||
{$currentUser || '???'}
|
||||
</div>
|
||||
<button on:click={() => (changePasswordModal = true)}>
|
||||
<Icon name="lock" />{$i18n.t('Change password')}</button
|
||||
>
|
||||
<button on:click={() => logout()}> <Icon name="log-out" />{$i18n.t('Log out')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if changePasswordModal}
|
||||
<Modal on:close={() => (changePasswordModal = false)}>
|
||||
<h2>
|
||||
<Icon name="lock" />
|
||||
{$i18n.t('Change password')}
|
||||
</h2>
|
||||
<form class="change-password">
|
||||
<label>
|
||||
<span>{$i18n.t('Current password')}</span>
|
||||
<input type="password" bind:value={currentPassword} required />
|
||||
</label>
|
||||
<label>
|
||||
<span>{$i18n.t('New password')}</span>
|
||||
<input type="password" bind:value={newPassword} required />
|
||||
</label>
|
||||
<label>
|
||||
<span>{$i18n.t('Confirm new password')}</span>
|
||||
<input type="password" bind:value={newPasswordConfirm} required />
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
on:click={() => changePassword()}
|
||||
disabled={newPassword !== newPasswordConfirm || !newPassword}
|
||||
>{$i18n.t('Change password')}</button
|
||||
>
|
||||
</form>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.user-dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
background: var(--background);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--foreground);
|
||||
padding: 0.5em;
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
right: 0.5rem;
|
||||
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.5);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.user {
|
||||
font-weight: 500;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
button,
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.change-password {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: center;
|
||||
gap: 0.5rem 1rem;
|
||||
|
||||
& label {
|
||||
display: contents;
|
||||
|
||||
& span {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
& button {
|
||||
grid-column: span 2;
|
||||
justify-content: center;
|
||||
justify-self: center;
|
||||
font-weight: 500;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,59 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
export let disabled = false;
|
||||
export let dismissable = true;
|
||||
|
||||
function close() {
|
||||
if (!dismissable) return;
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:body on:keydown={onKeydown} />
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
||||
<div class="modal-container" on:click={close} transition:fade={{ duration: 200 }}>
|
||||
<div class="modal" class:disabled on:click|stopPropagation>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.66);
|
||||
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--foreground);
|
||||
padding: 2rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
&.disabled {
|
||||
filter: brightness(0.66);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -20,7 +20,7 @@
|
|||
$: if (editing && selector) selector.focus();
|
||||
$: if (!focus && !hover) editing = false;
|
||||
|
||||
function onInput(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
function onInput(ev: CustomEvent<SelectorValue>) {
|
||||
newValue = ev.detail;
|
||||
selector.focus();
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
padding: 0.25em;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
export let hide = false;
|
||||
let hidden = true;
|
||||
</script>
|
||||
|
@ -8,35 +6,28 @@
|
|||
<section class="labelborder" class:hide class:hidden>
|
||||
<header
|
||||
on:click={() => {
|
||||
if (hide) hidden = !hidden;
|
||||
if (hide) {
|
||||
hidden = !hidden;
|
||||
}
|
||||
}}
|
||||
on:keydown={(ev) => {
|
||||
if (['Space', 'Enter'].includes(ev.key) && hide) hidden = !hidden;
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<slot name="header-full">
|
||||
<h3>
|
||||
<slot name="header" />
|
||||
</h3>
|
||||
<h3><slot name="header" /></h3>
|
||||
</slot>
|
||||
</header>
|
||||
{#if !hide || !hidden}
|
||||
<div class="content" transition:slide>
|
||||
<slot />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="hidden-indicator" />
|
||||
{/if}
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
section.labelborder {
|
||||
margin-top: 0.66rem;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
& header {
|
||||
header {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
|
@ -45,24 +36,28 @@
|
|||
padding-bottom: 0.33rem;
|
||||
margin-bottom: 0.33rem;
|
||||
|
||||
& h3 {
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide {
|
||||
& header {
|
||||
header {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide.hidden {
|
||||
opacity: 0.66;
|
||||
}
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
&.hidden {
|
||||
opacity: 0.66;
|
||||
|
||||
& .hidden-indicator {
|
||||
border-top: 1px solid var(--foreground);
|
||||
margin-top: calc(-0.33rem + 2px);
|
||||
header {
|
||||
border-bottom-width: 0.5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import LabelBorder from './LabelBorder.svelte';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { format } from 'date-fns';
|
||||
import Icon from '$lib/components/utils/Icon.svelte';
|
||||
const dispatch = createEventDispatcher<{ change: WidgetChange }>();
|
||||
|
||||
export let address: string;
|
||||
|
@ -35,7 +34,7 @@
|
|||
</script>
|
||||
|
||||
<LabelBorder hide={!notes?.length}>
|
||||
<span slot="header"><Icon plain name="note" /> {$i18n.t('Notes')}</span>
|
||||
<span slot="header">Notes</span>
|
||||
<div class="notes" contenteditable on:input={onInput} bind:this={contentEl}>
|
||||
{#each (notes || '\n').split('\n') as line, idx}
|
||||
{#if idx > 0}<br />{/if}
|
||||
|
|
|
@ -93,10 +93,7 @@
|
|||
import debug from 'debug';
|
||||
import Spinner from './Spinner.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
input: SelectorValue | undefined;
|
||||
focus: boolean;
|
||||
}>();
|
||||
const dispatch = createEventDispatcher();
|
||||
const dbg = debug('kestrel:Selector');
|
||||
let selectorEl: HTMLElement;
|
||||
|
||||
|
@ -148,7 +145,6 @@
|
|||
current = undefined;
|
||||
dispatch('input', current);
|
||||
}
|
||||
$: if (!inputValue) reset();
|
||||
|
||||
let options: SelectorOption[] = [];
|
||||
let searchResult: UpListing | undefined = undefined;
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
import Selector, { type SelectorValue } from '../utils/Selector.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { WidgetChange } from '$lib/types/base';
|
||||
import { addEmitter } from '$lib/components/AddModal.svelte';
|
||||
import debug from 'debug';
|
||||
const dispatch = createEventDispatcher();
|
||||
const dbg = debug(`kestrel:EntityList`);
|
||||
|
@ -124,7 +123,7 @@
|
|||
|
||||
$: if (adding && addSelector) addSelector.focus();
|
||||
|
||||
function addEntity(ev: CustomEvent<SelectorValue | undefined>) {
|
||||
function addEntity(ev: CustomEvent<SelectorValue>) {
|
||||
dbg('Adding entity', ev.detail);
|
||||
const addAddress = ev.detail?.t == 'Address' ? ev.detail.c : undefined;
|
||||
if (!addAddress) return;
|
||||
|
@ -154,7 +153,7 @@
|
|||
{/if}
|
||||
<div class="items">
|
||||
{#each sortedEntities as entity (entity)}
|
||||
<div data-address={entity} data-select-mode={select} use:observe class="item">
|
||||
<div data-address={entity} data-select-mode={select} use:observe class="row">
|
||||
{#if visible.has(entity)}
|
||||
{#if thumbnails}
|
||||
<UpObjectCard
|
||||
|
@ -167,12 +166,7 @@
|
|||
}}
|
||||
/>
|
||||
<div class="icon">
|
||||
<IconButton
|
||||
plain
|
||||
name="trash"
|
||||
color="#dc322f"
|
||||
on:click={() => removeEntity(entity)}
|
||||
/>
|
||||
<IconButton name="trash" color="#dc322f" on:click={() => removeEntity(entity)} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="object">
|
||||
|
@ -187,12 +181,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<IconButton
|
||||
plain
|
||||
name="trash"
|
||||
color="#dc322f"
|
||||
on:click={() => removeEntity(entity)}
|
||||
/>
|
||||
<IconButton name="trash" color="#dc322f" on:click={() => removeEntity(entity)} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
|
@ -203,39 +192,26 @@
|
|||
{#if address}
|
||||
<div class="add">
|
||||
{#if adding}
|
||||
<div class="main">
|
||||
<Selector
|
||||
bind:this={addSelector}
|
||||
placeholder={$i18n.t('Add or create an entry') || ''}
|
||||
types={['Address', 'NewAddress']}
|
||||
on:input={addEntity}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) {
|
||||
adding = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Selector
|
||||
bind:this={addSelector}
|
||||
placeholder={$i18n.t('Search database or paste an URL') || ''}
|
||||
types={['Address', 'NewAddress']}
|
||||
on:input={addEntity}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) {
|
||||
adding = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="main">
|
||||
<IconButton
|
||||
name="plus-circle"
|
||||
outline
|
||||
subdued
|
||||
on:click={() => {
|
||||
adding = true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if address}
|
||||
<IconButton
|
||||
outline
|
||||
subdued
|
||||
name="upload"
|
||||
title={$i18n.t('Upload a file') || ''}
|
||||
on:click={() => addEmitter.emit('destination', address || '')}
|
||||
/>
|
||||
{/if}
|
||||
<IconButton
|
||||
name="plus-circle"
|
||||
outline
|
||||
subdued
|
||||
on:click={() => {
|
||||
adding = true;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -271,7 +247,7 @@
|
|||
align-items: stretch;
|
||||
}
|
||||
|
||||
.item {
|
||||
.row {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -283,12 +259,10 @@
|
|||
}
|
||||
|
||||
.entitylist:not(.has-thumbnails) {
|
||||
.item {
|
||||
.row {
|
||||
display: flex;
|
||||
.object {
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@ -306,7 +280,7 @@
|
|||
}
|
||||
|
||||
.entitylist.has-thumbnails {
|
||||
.item {
|
||||
.row {
|
||||
position: relative;
|
||||
|
||||
.icon {
|
||||
|
@ -324,21 +298,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.add {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
|
||||
& .main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.entitylist.style-grid .add {
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
import { i18n } from '$lib/i18n';
|
||||
import UpLink from '../display/UpLink.svelte';
|
||||
import { ATTR_ADDED, ATTR_LABEL } from '@upnd/upend/constants';
|
||||
import UpAttribute from '$lib/components/display/UpAttribute.svelte';
|
||||
import UpValue from '$lib/components/display/UpValue.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{ change: WidgetChange }>();
|
||||
|
||||
|
@ -34,14 +36,13 @@
|
|||
|
||||
const TIMESTAMP_COL = 'timestamp';
|
||||
const PROVENANCE_COL = 'provenance';
|
||||
const USER_COL = 'user';
|
||||
const ENTITY_COL = 'entity';
|
||||
const ATTR_COL = 'attribute';
|
||||
const VALUE_COL = 'value';
|
||||
|
||||
$: templateColumns = (
|
||||
(displayColumns || []).map((column, idx) => {
|
||||
if (columnWidths?.[idx]) return columnWidths[idx];
|
||||
if (columnWidths?.[idx]) return columnWidths?.[idx];
|
||||
return 'minmax(6em, auto)';
|
||||
}) as string[]
|
||||
)
|
||||
|
@ -57,7 +58,7 @@
|
|||
let newEntryValue: SelectorValue | undefined;
|
||||
|
||||
$: if (adding && newAttrSelector) newAttrSelector.focus();
|
||||
$: if (!addFocus && !addHover && !newEntryAttribute && !newEntryValue) adding = false;
|
||||
$: if (!addFocus && !addHover) adding = false;
|
||||
|
||||
async function addEntry(attribute: string, value: SelectorValue) {
|
||||
dispatch('change', {
|
||||
|
@ -189,31 +190,11 @@
|
|||
const COLUMN_LABELS: { [key: string]: string } = {
|
||||
timestamp: $i18n.t('Added at'),
|
||||
provenance: $i18n.t('Provenance'),
|
||||
user: $i18n.t('User'),
|
||||
entity: $i18n.t('Entity'),
|
||||
attribute: $i18n.t('Attribute'),
|
||||
value: $i18n.t('Value')
|
||||
};
|
||||
|
||||
function formatValue(value: string | number | null, attribute: string): string {
|
||||
try {
|
||||
switch (attribute) {
|
||||
case 'FILE_SIZE':
|
||||
return filesize(parseInt(String(value), 10), { base: 2 });
|
||||
case ATTR_ADDED:
|
||||
case 'LAST_VISITED':
|
||||
return formatRelative(fromUnixTime(parseInt(String(value), 10)), new Date());
|
||||
case 'NUM_VISITED':
|
||||
return `${value} times`;
|
||||
case 'MEDIA_DURATION':
|
||||
return formatDuration(parseInt(String(value), 10));
|
||||
}
|
||||
} catch {
|
||||
// noop.
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// Unused attributes
|
||||
let unusedAttributes: string[] = [];
|
||||
|
||||
|
@ -245,16 +226,6 @@
|
|||
</div>
|
||||
{:else if column == PROVENANCE_COL}
|
||||
<div class="cell">{entry.provenance}</div>
|
||||
{:else if column == USER_COL}
|
||||
<div class="cell">
|
||||
{#if entry.user}
|
||||
{entry.user}
|
||||
{:else}
|
||||
<div class="unset">
|
||||
{$i18n.t('unset')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if column == ENTITY_COL}
|
||||
<div class="cell entity mark-entity">
|
||||
<UpObject
|
||||
|
@ -267,42 +238,19 @@
|
|||
/>
|
||||
</div>
|
||||
{:else if column == ATTR_COL}
|
||||
<div
|
||||
class="cell mark-attribute"
|
||||
class:formatted={Boolean(Object.keys($attributeLabels).includes(entry.attribute))}
|
||||
>
|
||||
<UpLink to={{ attribute: entry.attribute }}>
|
||||
<Ellipsis
|
||||
value={$attributeLabels[entry.attribute] || entry.attribute}
|
||||
title={$attributeLabels[entry.attribute]
|
||||
? `${$attributeLabels[entry.attribute]} (${entry.attribute})`
|
||||
: entry.attribute}
|
||||
/>
|
||||
</UpLink>
|
||||
<div class="cell">
|
||||
<UpAttribute attribute={entry.attribute} />
|
||||
</div>
|
||||
{:else if column == VALUE_COL}
|
||||
<div
|
||||
class="cell value mark-value"
|
||||
data-address={entry.value.t === 'Address' ? entry.value.c : undefined}
|
||||
>
|
||||
<Editable value={entry.value} on:edit={(ev) => updateEntry(entry, ev.detail)}>
|
||||
{#if entry.value.t === 'Address'}
|
||||
<UpObject
|
||||
link
|
||||
address={String(entry.value.c)}
|
||||
labels={$labelListing?.getObject(String(entry.value.c))?.identify() || []}
|
||||
on:resolved={(event) => {
|
||||
addSortKeys(String(entry.value.c), event.detail, true);
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class:formatted={Boolean(formatValue(entry.value.c, entry.attribute))}>
|
||||
<Ellipsis
|
||||
value={formatValue(entry.value.c, entry.attribute) || String(entry.value.c)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</Editable>
|
||||
<div class="cell">
|
||||
<UpValue
|
||||
value={entry.value}
|
||||
labels={$labelListing?.getObject(String(entry.value.c))?.identify() || []}
|
||||
on:edit={(ev) => updateEntry(entry, ev.detail)}
|
||||
on:resolved={(event) => {
|
||||
addSortKeys(String(entry.value.c), event.detail, true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div>?</div>
|
||||
|
@ -364,8 +312,7 @@
|
|||
<div class="cell mark-attribute">
|
||||
<Selector
|
||||
types={['Attribute', 'NewAttribute']}
|
||||
on:input={(ev) =>
|
||||
(newEntryAttribute = ev.detail?.t === 'Attribute' ? ev.detail?.name : '')}
|
||||
on:input={(ev) => (newEntryAttribute = ev.detail.name)}
|
||||
on:focus={(ev) => (addFocus = ev.detail)}
|
||||
keepFocusOnSet
|
||||
bind:this={newAttrSelector}
|
||||
|
@ -428,7 +375,7 @@
|
|||
|
||||
.attr-action {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
<script lang="ts">
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
import { Query, type UpListing } from '@upnd/upend';
|
||||
import { Any } from '@upnd/upend/query';
|
||||
import type { Address } from '@upnd/upend/types';
|
||||
import { query } from '$lib/entity';
|
||||
import UpObject from '../display/UpObject.svelte';
|
||||
import UpObjectCard from '../display/UpObjectCard.svelte';
|
||||
import { ATTR_LABEL } from '@upnd/upend/constants';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import IconButton from '../utils/IconButton.svelte';
|
||||
import Selector, { type SelectorValue } from '../utils/Selector.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { WidgetChange } from '$lib/types/base';
|
||||
import debug from 'debug';
|
||||
import api from '$lib/api';
|
||||
import UpAttribute from '$lib/components/display/UpAttribute.svelte';
|
||||
import UpValue from '$lib/components/display/UpValue.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
const dbg = debug(`kestrel:Table`);
|
||||
|
||||
export let entities: Address[];
|
||||
export let sort = true;
|
||||
export let address: Address | undefined = undefined;
|
||||
export let columns: string[] = [];
|
||||
|
||||
let currentColumns: string[] = columns;
|
||||
|
||||
let values: UpListing | undefined = undefined;
|
||||
$: update(entities, currentColumns);
|
||||
async function update(entities: Address[], columns: string[]) {
|
||||
values = await api.query(
|
||||
Query.matches(
|
||||
entities.map((entity) => `@${entity}`),
|
||||
columns,
|
||||
Any
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$: deduplicatedEntities = Array.from(new Set(entities));
|
||||
|
||||
// Sorting
|
||||
let sortedEntities: Address[] = [];
|
||||
|
||||
let sortKeys: { [key: string]: string[] } = {};
|
||||
function addSortKeys(key: string, vals: string[], resort: boolean) {
|
||||
if (!sortKeys[key]) {
|
||||
sortKeys[key] = [];
|
||||
}
|
||||
let changed = false;
|
||||
vals.forEach((val) => {
|
||||
if (!sortKeys[key].includes(val)) {
|
||||
changed = true;
|
||||
sortKeys[key].push(val);
|
||||
}
|
||||
});
|
||||
|
||||
if (resort && changed) sortEntities();
|
||||
}
|
||||
|
||||
function sortEntities() {
|
||||
if (!sort) return;
|
||||
|
||||
sortedEntities = deduplicatedEntities.concat();
|
||||
|
||||
sortedEntities.sort((a, b) => {
|
||||
if (!sortKeys[a]?.length || !sortKeys[b]?.length) {
|
||||
if (Boolean(sortKeys[a]?.length) && !sortKeys[b]?.length) {
|
||||
return -1;
|
||||
} else if (!sortKeys[a]?.length && Boolean(sortKeys[b]?.length)) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
} else {
|
||||
return sortKeys[a][0].localeCompare(sortKeys[b][0], undefined, {
|
||||
numeric: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Labelling
|
||||
let labelListing: Readable<UpListing | undefined> = readable(undefined);
|
||||
$: {
|
||||
const addressesString = deduplicatedEntities.map((addr) => `@${addr}`).join(' ');
|
||||
|
||||
labelListing = query(`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`).result;
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($labelListing) {
|
||||
deduplicatedEntities.forEach((address) => {
|
||||
addSortKeys(address, $labelListing?.getObject(address).identify() || [], false);
|
||||
});
|
||||
sortEntities();
|
||||
}
|
||||
}
|
||||
|
||||
if (!sort) {
|
||||
sortedEntities = entities;
|
||||
}
|
||||
|
||||
// Visibility
|
||||
let visible: Set<string> = new Set();
|
||||
let observer = new IntersectionObserver((intersections) => {
|
||||
intersections.forEach((intersection) => {
|
||||
const address = (intersection.target as HTMLElement).dataset['address'];
|
||||
if (!address) {
|
||||
console.warn('Intersected wrong element?');
|
||||
return;
|
||||
}
|
||||
|
||||
if (intersection.isIntersecting) {
|
||||
visible.add(address);
|
||||
}
|
||||
visible = visible;
|
||||
});
|
||||
});
|
||||
|
||||
function observe(node: HTMLElement) {
|
||||
observer.observe(node);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
observer.unobserve(node);
|
||||
}
|
||||
};
|
||||
}
|
||||
//
|
||||
// // Adding
|
||||
// let addSelector: Selector | undefined;
|
||||
// let adding = false;
|
||||
//
|
||||
// $: if (adding && addSelector) addSelector.focus();
|
||||
//
|
||||
// function addEntity(ev: CustomEvent<SelectorValue>) {
|
||||
// dbg('Adding entity', ev.detail);
|
||||
// const addAddress = ev.detail?.t == 'Address' ? ev.detail.c : undefined;
|
||||
// if (!addAddress) return;
|
||||
//
|
||||
// dispatch('change', {
|
||||
// type: 'entry-add',
|
||||
// address: addAddress
|
||||
// } as WidgetChange);
|
||||
// }
|
||||
//
|
||||
// function removeEntity(address: string) {
|
||||
// if (confirm($i18n.t('Are you sure you want to remove this entry from members?') || '')) {
|
||||
// dbg('Removing entity', address);
|
||||
// dispatch('change', {
|
||||
// type: 'entry-delete',
|
||||
// address
|
||||
// } as WidgetChange);
|
||||
// }
|
||||
// }
|
||||
</script>
|
||||
|
||||
<div class="table" style="--columns-count: {currentColumns.length}">
|
||||
{#if !sortedEntities.length}
|
||||
<div class="message">
|
||||
{$i18n.t('No entries.')}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="header">
|
||||
<div>
|
||||
{$i18n.t('Name')}
|
||||
</div>
|
||||
{#each currentColumns as attribute}
|
||||
<UpAttribute {attribute} />
|
||||
{/each}
|
||||
</div>
|
||||
{#each sortedEntities as entity (entity)}
|
||||
<div data-address={entity} use:observe class="row" class:visible={visible.has(entity)}>
|
||||
{#if visible.has(entity)}
|
||||
<div class="object">
|
||||
<UpObject
|
||||
link
|
||||
address={entity}
|
||||
labels={sortKeys[entity]}
|
||||
on:resolved={(event) => {
|
||||
addSortKeys(entity, event.detail, true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#each currentColumns as attribute}
|
||||
{@const value = values?.getObject(entity)?.get(attribute)}
|
||||
{#if value}
|
||||
<UpValue {value} {attribute} />
|
||||
{:else}
|
||||
<div class="null">X</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="skeleton" style="text-align: center">...</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../styles/colors';
|
||||
|
||||
.table {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr repeat(var(--columns-count), 1fr);
|
||||
|
||||
gap: 0.2em;
|
||||
|
||||
.header,
|
||||
.row.visible {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.row:not(.visible) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -22,7 +22,6 @@ select {
|
|||
font-size: 2em;
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
border: 1px solid var(--foreground);
|
||||
border-radius: 4px;
|
||||
|
@ -53,23 +52,6 @@ button,
|
|||
}
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='password'] {
|
||||
padding: 0.25em;
|
||||
|
||||
border: 1px solid var(--foreground-lighter);
|
||||
border-radius: 4px;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
|
||||
transition: box-shadow 0.25s;
|
||||
|
||||
&:focus {
|
||||
box-shadow: -1px -1px 2px 2px var(--primary);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mark-entity::first-letter,
|
||||
.mark-entity *::first-letter {
|
||||
color: color.scale(color.mix(colors.$base1, colors.$red), $saturation: -33%);
|
||||
|
|
|
@ -1,22 +1,9 @@
|
|||
import api from '$lib/api';
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { VaultInfo } from '@upnd/upend/types';
|
||||
import type { VaultOptions } from '@upnd/upend/api';
|
||||
|
||||
export const vaultInfo: Readable<VaultInfo | undefined> = readable(
|
||||
undefined as VaultInfo | undefined,
|
||||
(set) => {
|
||||
api.fetchInfo().then(async (info: VaultInfo) => {
|
||||
set(info);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const vaultOptions: Readable<VaultOptions | undefined> = readable(
|
||||
undefined as VaultOptions | undefined,
|
||||
(set) => {
|
||||
api.fetchOptions().then(async (options: VaultOptions) => {
|
||||
set(options);
|
||||
});
|
||||
}
|
||||
);
|
||||
export const vaultInfo = readable(undefined as VaultInfo | undefined, (set) => {
|
||||
api.fetchInfo().then(async (info: VaultInfo) => {
|
||||
set(info);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,10 +2,15 @@ import api from '$lib/api';
|
|||
import { i18n } from '$lib/i18n';
|
||||
import { derived, readable, type Readable } from 'svelte/store';
|
||||
import type { AttributeListingResult } from '@upnd/upend/types';
|
||||
import debug from 'debug';
|
||||
|
||||
const dbg = debug('kestrel:labels');
|
||||
|
||||
const databaseAttributeLabels: Readable<{ [key: string]: string }> = readable({}, (set) => {
|
||||
const result: Record<string, string> = {};
|
||||
dbg('Fetching all attributes');
|
||||
api.fetchAllAttributes().then((attributes: AttributeListingResult) => {
|
||||
dbg('Fetched all attributes: %o', attributes);
|
||||
attributes.forEach((attribute) => {
|
||||
if (attribute.labels.length) {
|
||||
result[attribute.name] = attribute.labels.sort()[0];
|
||||
|
|
|
@ -4,17 +4,6 @@
|
|||
import Footer from '$lib/components/layout/Footer.svelte';
|
||||
import DropPasteHandler from '$lib/components/DropPasteHandler.svelte';
|
||||
import AddModal from '$lib/components/AddModal.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { vaultInfo, vaultOptions } from '$lib/util/info';
|
||||
import LoginModal from '$lib/components/LoginModal.svelte';
|
||||
import { currentUser } from '$lib/api';
|
||||
|
||||
onMount(() => {
|
||||
if ($vaultOptions && !$vaultOptions.blob_mode) {
|
||||
goto('/setup');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Header />
|
||||
|
@ -24,7 +13,4 @@
|
|||
<Footer />
|
||||
|
||||
<AddModal />
|
||||
{#if $vaultInfo && !$vaultInfo.public && !$currentUser}
|
||||
<LoginModal />
|
||||
{/if}
|
||||
<DropPasteHandler />
|
||||
<DropPasteHandler />>
|
||||
|
|
|
@ -157,6 +157,14 @@
|
|||
}
|
||||
];
|
||||
|
||||
fetch('/api/options')
|
||||
.then((res) => res.json())
|
||||
.then((options) => {
|
||||
if (!options.blob_mode) {
|
||||
goto('/setup');
|
||||
}
|
||||
});
|
||||
|
||||
$: updateTitle($vaultInfo?.name || $i18n.t('Home') || 'Home');
|
||||
</script>
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
let root: HTMLDivElement;
|
||||
let identities: string[] = [];
|
||||
$: addresses = $page.params.addresses.split(',');
|
||||
let backgrounds: Record<string, string | undefined> = {};
|
||||
|
||||
function add(value: SelectorValue) {
|
||||
if (value.t !== 'Address') return;
|
||||
|
@ -97,6 +98,7 @@
|
|||
{only}
|
||||
on:close={() => close(index)}
|
||||
on:detail={(ev) => onDetailChanged(index, ev)}
|
||||
background="var(--background-lightest)"
|
||||
>
|
||||
<CombineColumn spec={address} on:close={() => close(index)} />
|
||||
</BrowseColumn>
|
||||
|
@ -109,6 +111,7 @@
|
|||
close(index);
|
||||
}}
|
||||
on:detail={(ev) => onDetailChanged(index, ev)}
|
||||
background="var(--background-lightest)"
|
||||
>
|
||||
<SelectedColumn />
|
||||
</BrowseColumn>
|
||||
|
@ -145,13 +148,20 @@
|
|||
{address}
|
||||
{index}
|
||||
{only}
|
||||
background={backgrounds[address]}
|
||||
on:close={() => close(index)}
|
||||
on:resolved={(ev) => onIdentified(index, ev)}
|
||||
on:detail={(ev) => onDetailChanged(index, ev)}
|
||||
on:combine={() => addCombine(address)}
|
||||
let:detail
|
||||
>
|
||||
<Inspect {address} {detail} on:resolved on:close />
|
||||
<Inspect
|
||||
{address}
|
||||
{detail}
|
||||
on:resolved
|
||||
on:close
|
||||
on:background={(ev) => (backgrounds[address] = ev.detail)}
|
||||
/>
|
||||
</BrowseColumn>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
import Table from '../lib/components/widgets/Table.svelte';
|
||||
import { imageAddress, imageVerticalAddress, videoAddress, videoVerticalAddress } from './common';
|
||||
|
||||
const meta: Meta<Table> = {
|
||||
title: 'Widgets/Table',
|
||||
component: Table,
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
entities: [imageAddress, imageVerticalAddress, videoAddress, videoVerticalAddress]
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<Table>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Mixed: Story = {
|
||||
args: {
|
||||
columns: ['MEDIA_DURATION', 'FILE_SIZE']
|
||||
}
|
||||
};
|
|
@ -42,12 +42,6 @@ export const Link: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const Keyed: Story = {
|
||||
args: {
|
||||
address: 'zb2rhmpmTFPxdhaxTQg5Ug3KHFU8DZNUPh8TaPY2v8UQVJbQk'
|
||||
}
|
||||
};
|
||||
|
||||
export const Banner: Story = {
|
||||
args: {
|
||||
banner: true
|
||||
|
@ -61,13 +55,6 @@ export const BannerWithLabels: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const KeyedBanner: Story = {
|
||||
args: {
|
||||
address: 'zb2rhmpmTFPxdhaxTQg5Ug3KHFU8DZNUPh8TaPY2v8UQVJbQk',
|
||||
banner: true
|
||||
}
|
||||
};
|
||||
|
||||
export const Overflow: Story = {
|
||||
args: {
|
||||
labels: ['qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'.repeat(3)]
|
||||
|
|
|
@ -1,19 +1,9 @@
|
|||
import { sentrySvelteKit } from '@sentry/sveltekit';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
sentrySvelteKit({
|
||||
sourceMapsUploadOptions: {
|
||||
org: 'upend',
|
||||
project: 'upend-kestrel',
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN
|
||||
}
|
||||
}),
|
||||
sveltekit()
|
||||
],
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
|
Loading…
Reference in New Issue