mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-10-30 04:49:32 -04:00
CI green on sources: runner toolchain, clippy, typecheck, tests compile (#3102)
* ci: install rustfmt and clippy components on every runner
Self-hosted runners have rustc preinstalled, so setup-rust skipped the
dtolnay/rust-toolchain step that added the components and the Rust
Formatting job died with 'cargo-fmt is not installed'. Add the components
to rust-toolchain.toml and run rustup component add unconditionally.
* setup: install FFmpeg development libraries on Linux and macOS
crates/ffmpeg is a default workspace member that links libav* through
ffmpeg-sys-next, so a fresh machine fails the first cargo build with
'libavutil.pc not found'. Install the libav dev packages and pkg-config
from scripts/setup.sh for every supported package manager, warn from
xtask setup when pkg-config cannot find them, and document the requirement
in the README build section.
* test: call the trash directly in delete_strategy_test
bcc7337 gave LocalDeleteStrategy::move_to_trash a JobContext parameter so
it can record where the item went; the integration test still passed one
argument and stopped every cargo test target from compiling. The two trash
cases now call ops::files::trash::trash with the same arguments the
strategy forwards, which is what they were asserting on.
* interface: clear the 30 Tauri typecheck errors
Remove the 23 unused imports and locals, apply the rename rules' serde
defaults (regex and whole_name false, start and step 1) where the modal
renders optional fields, drop the size prop ToggleGroup never accepted,
and type SourceCard on ts-client's SourceInfo so a filesystem source with
no adapter renders its data type instead. Register the /redundancy/duplicates
route the dashboard already navigates to, which is what the DuplicatesView
import was for.
* clippy: make cargo clippy --workspace -D warnings pass on Linux
sd-store names its sqlx row tuples with type aliases. sd-ffmpeg converts
with try_from and f32::from where the cast was lossless, takes raw
borrows for FFI out-pointers, resamples from an integer position, and
allows the pointer-alignment and s32-to-f32 precision casts with the
reason. sd-mobile-core sets DaemonRequest.device (None, local execution)
on the two envelopes it builds. file-opening-macos and file-opening-windows
pull their platform bindings only on that target and gate their sources,
matching sd-desktop-macos. The Tauri build script drops externalBin from
the config when no daemon sidecar exists, so a check or clippy run that
never built the daemon still passes, and the desktop binary's macOS-only
commands return on other hosts without unreachable code. Workspace
rust-version moves to 1.95, the floor gpui already imposes, so the MSRV
lint stops flagging APIs the pinned 1.97.1 toolchain uses.
* docs: make the stale doctests compile
sd-media-metadata examples still imported from the old image module and
asserted a plus code that belonged to the other example's coordinates (the
value is verified against the reference Open Location Code encoder);
MediaLocation has no Display, so the parse example compares coordinates.
The sd-task-system example unwraps dispatch and matches the (TaskId,
TaskOutput) pair Done now carries. The spacedrive-sdk sketch names types
that do not exist yet and the job-derive expansion only resolves inside
sd-core, so both are marked ignore with the reason beside them.
* build: pin bun to 1.3.4 everywhere
package.json said bun@1.3.0 while CI installed 1.3.4, and any newer bun
rewrites bun.lockb on install, so the lockfile churned on every machine.
Pin packageManager to the CI version, document it, and install with
--frozen-lockfile in the TypeScript job so a drifting lockfile fails
instead of being silently rewritten.
* status: register the CI green on sources work and the libav requirement
* docs: make the sd-core doctests compile too
Five sd-core examples referenced the crate by its old name, used an arrow
glyph in a rust fence, or called methods without the trait in scope; the
copy strategy sketch needs a running JobContext so it is marked ignore.
* ci: stop shrinking the runner's root disk before installing a toolchain
easimon/maximize-build-space moves the root filesystem's free space into an
LVM volume mounted at the workspace and leaves root with the reserve, which
on the Blacksmith runner came out at 100 MB after the swap file. rustup
installs the pinned 1.97.1 toolchain under /home/runner/.rustup on root, so
both Rust jobs died with 'No space left on device'. The runner has 82 GB
free on root, which is plenty for a clippy target directory, so the step
goes.
* ci: run clippy on the Ubuntu 24.04 Blacksmith runner; let sd-server check without bun
The Blacksmith migration wrote the runner label into the matrix's target
field and left runs-on at GitHub's ubuntu-22.04, whose FFmpeg 4.4 predates
the libav 6 API sd-ffmpeg uses (ch_layout, av_dict_iterate), so the job
could never compile sd-ffmpeg. It now runs on blacksmith-4vcpu-ubuntu-2404
with FFmpeg 6.1, the same as a developer machine on 24.04, and the target
field holds a target triple again.
sd-server's build script skipped the web build when bun was absent and
RustEmbed then failed on the missing apps/web/dist. A debug build now gets
an empty dist with a warning so check and clippy pass; a release build
without bun and without a prebuilt dist still refuses, so a server never
ships without its UI by accident.
* build: let the desktop crate compile without a built frontend
tauri::generate_context! panics when frontendDist is missing, which is how
every check or clippy run looks before bun run build. The build script now
creates an empty apps/tauri/dist for debug builds with a warning; a release
build without one still refuses.
* status: record the CI result for the sources green-up
This commit is contained in:
1 parent
9929d22c72
commit
61dea60558
61 files changed
+434
-392
No files matched your search
@@ -47,6 +47,15 @@ runs:
|
||||
run: |
|
||||
rustup target add ${{ inputs.target }}
|
||||
|
||||
# Self-hosted runners ship a toolchain without rustfmt or clippy, and
|
||||
# rust-toolchain.toml selects a channel the preinstalled toolchain may not
|
||||
# have yet, so install both components for the pinned channel every run.
|
||||
- name: Ensure rustfmt and clippy components
|
||||
shell: bash
|
||||
run: |
|
||||
rustup show active-toolchain || rustup toolchain install
|
||||
rustup component add rustfmt clippy
|
||||
|
||||
- name: Verify Rust installation
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -29,17 +29,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: easimon/maximize-build-space@master
|
||||
with:
|
||||
swap-size-mb: 3072
|
||||
root-reserve-mb: 6144
|
||||
remove-dotnet: "true"
|
||||
remove-codeql: "true"
|
||||
remove-haskell: "true"
|
||||
remove-docker-images: "true"
|
||||
|
||||
- name: Symlink target to C:\
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: powershell
|
||||
@@ -93,25 +82,15 @@ jobs:
|
||||
fail-fast: false # Don't cancel other platforms if one fails
|
||||
matrix:
|
||||
settings:
|
||||
- host: ubuntu-22.04
|
||||
target: blacksmith-4vcpu-ubuntu-2404
|
||||
name: Clippy (${{ matrix.settings.host }})
|
||||
- host: blacksmith-4vcpu-ubuntu-2404
|
||||
label: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: Clippy (${{ matrix.settings.label }})
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: easimon/maximize-build-space@master
|
||||
with:
|
||||
swap-size-mb: 3072
|
||||
root-reserve-mb: 6144
|
||||
remove-dotnet: "true"
|
||||
remove-codeql: "true"
|
||||
remove-haskell: "true"
|
||||
remove-docker-images: "true"
|
||||
|
||||
- name: Symlink target to C:\
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: powershell
|
||||
@@ -194,7 +173,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
||||
run: bun install
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: TypeScript typecheck
|
||||
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@ resolver = "2"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/spacedriveapp/spacedrive"
|
||||
rust-version = "1.81"
|
||||
# gpui needs 1.95 (std::hint::cold_path); rust-toolchain.toml pins 1.97.1.
|
||||
rust-version = "1.95"
|
||||
|
||||
[workspace.dependencies]
|
||||
# First party dependencies
|
||||
|
||||
+8
-2
@@ -1,7 +1,7 @@
|
||||
# Project Status
|
||||
|
||||
> Last updated: 2026-09-25
|
||||
> Code baseline: `3ee1041f7`
|
||||
> Last updated: 2026-10-04
|
||||
> Code baseline: `9929d22c7`
|
||||
> Purpose: short-term working context across development sessions while
|
||||
> `.tasks/` is frozen.
|
||||
|
||||
@@ -196,6 +196,7 @@ Recent landed.
|
||||
| Addressing | proposed 2026-09-25; P0 and P1 ready, D1 to D6 open | unowned | Plan: `docs/plans/2026-09-25-addressing.md`. P1 first: content rows that hold each file's own integrity hash (today a file changed in place, or one sharing a sampled hash, can carry another file's hash, and compare-delete and dedupe trust it), network admission for fully paired devices only, and refusals for addresses that destructive operations can't honor. Then device UUIDs in `SdPath` with content and sidecars split out, the source-relative target and record reference, one resolver with scope and version checks, content addressing, and cloud identity. P0 rewrites `docs/core/addressing.mdx` to match the code. |
|
||||
| Locked and unmounted volumes | proposed 2026-09-28; nothing built | unowned | Plan: `docs/plans/2026-09-28-locked-volumes.md`. A source on an encrypted ZFS dataset whose key is not loaded reads as attached: the volume monitor never marks a vanished volume offline, `attach_library` trusts the stale `is_online` flag, and attachment is `Path::exists` on the empty mount point. The index survives through ordering and thresholds (restore before heal, sweeps only on whole-volume walks, the store's half-removal fuse, the snapshot collapse guard), but reads fail, and once the key loads the watch stays on the covered directory until a restart. L1 marks vanished volumes offline and attaches from live detection, L2 makes attached mean mounted with a mount point check before any walk, L3 reads ZFS `mounted` and `keystatus`, L4 follows mounts while running, L5 shows the state. One decision is James's: whether a locked source's index stays browsable. |
|
||||
| Retire Spacebot, Console, and storefront surfaces | done (2026-09-25) | Opus | The Spacebot UI, voice overlay, `spacebot` app config, window, build aliases, generated types, and design docs are removed; the Alt+Space global shortcut and its plugin went with the voice overlay. Console and Pod design docs are removed. Private copies are in `company/archive/spacedrive-sources-public-cleanup/`. No storefront code remained. |
|
||||
| CI green on `sources` | landed 2026-10-04 (Fable, SPAC-4); Rust Formatting, Clippy and TypeScript green on #3102 | Fable | The three `ci.yml` jobs were red on `sources` before any PR: the self-hosted runner's preinstalled toolchain had no rustfmt or clippy (`setup-rust` now installs both every run, and `rust-toolchain.toml` names them), `maximize-build-space` left the Blacksmith runner's root disk with 100 MB so no toolchain could install (step removed), the clippy job ran on GitHub's ubuntu-22.04 whose FFmpeg 4.4 cannot build `sd-ffmpeg` (now `blacksmith-4vcpu-ubuntu-2404`), and the server and desktop crates failed to compile without a built web UI (debug builds now get an empty dist with a warning; release builds still refuse), `cargo clippy --workspace -D warnings` failed in seven crates plus the Tauri sidecar check and the two platform-only file-opening crates, and `bun run typecheck` in `apps/tauri` had 30 errors in `packages/interface`. All fixed with no product change: sqlx row aliases in `sd-store`, exact casts and raw FFI borrows in `sd-ffmpeg`, `DaemonRequest.device` in `sd-mobile-core`, target-gated `file-opening-macos`/`-windows`, a Tauri build script that drops `externalBin` when no daemon sidecar exists, unused imports and `SourceInfo.adapter_id` nullability in the interface, the `/redundancy/duplicates` route the dashboard already linked. `core/tests/delete_strategy_test.rs` compiles again (calls `ops::files::trash::trash` directly), the 13 stale doctests compile, `scripts/setup.sh` installs the libav dev packages, bun is pinned to 1.3.4 with `--frozen-lockfile`, workspace `rust-version` is 1.95 (gpui's floor). Still red and out of this scope: the btrfs UUID parser unit test, two plus-code parser tests, the FTS5 fixture race under parallel `cargo test`, and 20 integration-test failures on a bare Linux VM (see SPAC-2). Next: confirm the three jobs green on the PR, then decide whether `core_tests.yml` should run on pull requests. |
|
||||
|
||||
## Product sequence
|
||||
|
||||
@@ -303,6 +304,11 @@ These are regression boundaries, not incidental fixes:
|
||||
left in place, reads as attached. Its index is kept, but reads fail, and
|
||||
after the volume returns its watch misses changes until the daemon restarts.
|
||||
See `docs/plans/2026-09-28-locked-volumes.md`.
|
||||
- `crates/ffmpeg` is a default workspace member and links the system libav
|
||||
libraries through `ffmpeg-sys-next`, so `cargo build` needs the FFmpeg
|
||||
development headers (`libav*-dev` and `libclang-dev` on Debian and Ubuntu,
|
||||
`ffmpeg` from Homebrew) before the first build. `scripts/setup.sh` installs
|
||||
them and `xtask setup` warns when pkg-config cannot find them.
|
||||
|
||||
## Recent landed
|
||||
|
||||
|
||||
@@ -236,8 +236,9 @@ types, so every interface shares one contract.
|
||||
|
||||
## Build from source
|
||||
|
||||
You need [Rust](https://rustup.rs/) stable with MSRV 1.81,
|
||||
[Bun](https://bun.sh) 1.3 or newer, Node 20, and
|
||||
You need [Rust](https://rustup.rs/) 1.95 or newer (`rust-toolchain.toml` pins 1.97.1),
|
||||
[Bun](https://bun.sh) 1.3.4 (the version `package.json` and CI pin; a newer
|
||||
Bun rewrites `bun.lockb`), Node 20, and
|
||||
[just](https://github.com/casey/just).
|
||||
|
||||
```bash
|
||||
@@ -260,6 +261,14 @@ just dev-server # Headless server and web interface
|
||||
just dev-mobile # Mobile client
|
||||
```
|
||||
|
||||
The `sd-ffmpeg` crate is a default workspace member and links the FFmpeg
|
||||
development libraries installed on the host (`libavutil`, `libavformat`,
|
||||
`libavcodec`, `libavfilter`, `libavdevice`, `libswscale`, `libswresample`) and
|
||||
uses `libclang` to generate its bindings. `scripts/setup.sh` installs them
|
||||
(`libav*-dev` and `libclang-dev` on Debian and Ubuntu, `ffmpeg` on Homebrew).
|
||||
Without them the first `cargo build` fails in `ffmpeg-sys-next` with
|
||||
`libavutil.pc not found`.
|
||||
|
||||
The default build can use FFmpeg installed on the host for video thumbnails and
|
||||
thumbstrips. Install the native codec bundle only when developing code that
|
||||
links the optional FFmpeg, libheif, or Pdfium libraries:
|
||||
|
||||
@@ -409,12 +409,14 @@ fn convert_jsonrpc_to_daemon_request(
|
||||
method: jsonrpc.method.clone(),
|
||||
library_id,
|
||||
payload,
|
||||
device: None,
|
||||
}
|
||||
} else if jsonrpc.method.starts_with("action:") {
|
||||
DaemonRequest::Action {
|
||||
method: jsonrpc.method.clone(),
|
||||
library_id,
|
||||
payload: jsonrpc.params.input.clone(),
|
||||
device: None,
|
||||
}
|
||||
} else {
|
||||
return Err(format!("Invalid method prefix: {}", jsonrpc.method));
|
||||
|
||||
@@ -166,7 +166,7 @@ pub fn config_from_env() -> (Option<u32>, bool) {
|
||||
let explicit = std::env::var("SD_GRID_CELLS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok());
|
||||
let bench = std::env::var("SD_GRID_BENCH").map_or(false, |v| v == "1");
|
||||
let bench = std::env::var("SD_GRID_BENCH").is_ok_and(|v| v == "1");
|
||||
(explicit.or(bench.then_some(DEFAULT_CELLS)), bench)
|
||||
}
|
||||
|
||||
@@ -468,11 +468,6 @@ impl GridView {
|
||||
true
|
||||
}
|
||||
|
||||
/// How many cells the current source is offering.
|
||||
pub fn len(&self) -> u32 {
|
||||
self.source.len()
|
||||
}
|
||||
|
||||
/// Columns and displayed cell size for the current width. While a pinch is
|
||||
/// live the column count is held and the tile scales continuously, so the
|
||||
/// grid overflows sideways; otherwise the row width is shared across
|
||||
@@ -493,7 +488,7 @@ impl GridView {
|
||||
|
||||
fn max_scroll(&self, width: f32, height: f32) -> f32 {
|
||||
let (cols, cell) = self.layout(width);
|
||||
let rows = (self.source.len() + cols - 1) / cols;
|
||||
let rows = self.source.len().div_ceil(cols);
|
||||
(rows as f32 * (cell + GAP) - height).max(0.0)
|
||||
}
|
||||
|
||||
@@ -799,7 +794,7 @@ impl GridView {
|
||||
}
|
||||
|
||||
self.stats.end_frame(t0, painted);
|
||||
if self.auto.is_some() && self.frame_index % 300 == 0 {
|
||||
if self.auto.is_some() && self.frame_index.is_multiple_of(300) {
|
||||
eprintln!(
|
||||
"frame {} | scroll {:.0} | cache {} tiles | rss {:.0} MB",
|
||||
self.frame_index,
|
||||
|
||||
@@ -59,6 +59,7 @@ pub trait TileSource {
|
||||
|
||||
/// Envelope edge in physical pixels: the buffer size a reader allocates,
|
||||
/// and the ceiling on any delivered [`Bitmap`].
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
fn tile(&self) -> u32;
|
||||
|
||||
/// Ask for the tile at `idx`. Idempotent while a request is outstanding,
|
||||
|
||||
+23
-7
@@ -11,13 +11,6 @@ fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If bun isn't available (e.g., Docker Rust build stage), the caller is
|
||||
// expected to have prebuilt apps/web/dist. Skip silently.
|
||||
if Command::new("bun").arg("--version").output().is_err() {
|
||||
println!("cargo:warning=bun not found on PATH — using existing apps/web/dist");
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
@@ -26,6 +19,29 @@ fn main() {
|
||||
.to_path_buf();
|
||||
let web_dir = repo_root.join("apps/web");
|
||||
|
||||
// If bun isn't available (e.g., Docker Rust build stage), the caller is
|
||||
// expected to have prebuilt apps/web/dist. RustEmbed needs the folder to
|
||||
// exist even for a check or clippy run, so a debug build without either
|
||||
// gets an empty one; a release build refuses to ship a server without a UI.
|
||||
if Command::new("bun").arg("--version").output().is_err() {
|
||||
let dist = web_dir.join("dist");
|
||||
if !dist.exists() {
|
||||
if env::var("PROFILE").as_deref() == Ok("release") {
|
||||
panic!(
|
||||
"bun not found on PATH and {} does not exist; build the web UI first or set SD_SKIP_WEB_BUILD=1 with a prebuilt dist",
|
||||
dist.display()
|
||||
);
|
||||
}
|
||||
std::fs::create_dir_all(&dist).expect("create an empty apps/web/dist");
|
||||
println!(
|
||||
"cargo:warning=bun not found on PATH — created an empty apps/web/dist so the server compiles without the web UI"
|
||||
);
|
||||
return;
|
||||
}
|
||||
println!("cargo:warning=bun not found on PATH — using existing apps/web/dist");
|
||||
return;
|
||||
}
|
||||
|
||||
// Refuse to proceed if workspace dependencies aren't installed.
|
||||
if !repo_root.join("node_modules").exists() {
|
||||
panic!(
|
||||
|
||||
@@ -5,9 +5,13 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
file-opening = { path = "../file-opening" }
|
||||
swift-rs = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[build-dependencies]
|
||||
# A plain workspace member is checked on every host, so the Swift bridge is
|
||||
# only pulled in where it can link.
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
swift-rs = "1.0"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
swift-rs = { version = "1.0", features = ["build"] }
|
||||
@@ -1,4 +1,5 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
swift_rs::SwiftLinker::new("11.0")
|
||||
.with_ios("11.0")
|
||||
.with_package("FileOpening", "./src-swift/")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(target_os = "macos")]
|
||||
|
||||
use file_opening::{FileOpener, OpenResult, OpenWithApp};
|
||||
use std::path::{Path, PathBuf};
|
||||
use swift_rs::*;
|
||||
|
||||
@@ -5,6 +5,10 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
file-opening = { path = "../file-opening" }
|
||||
|
||||
# A plain workspace member is checked on every host, so the Win32 bindings
|
||||
# are only pulled in where they exist.
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use file_opening::{FileOpener, OpenResult, OpenWithApp};
|
||||
use std::path::Path;
|
||||
use windows::core::*;
|
||||
|
||||
@@ -9,6 +9,7 @@ path = "src/main.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0", features = [] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[dependencies]
|
||||
# Tauri
|
||||
|
||||
@@ -60,6 +60,7 @@ fn main() {
|
||||
// Create target-suffixed daemon binary for Tauri bundler
|
||||
// Tauri's externalBin expects binaries with target triple suffix
|
||||
let target_triple = std::env::var("TARGET").expect("TARGET not set");
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
|
||||
|
||||
// Expose target triple to runtime code for daemon binary resolution
|
||||
println!("cargo:rustc-env=SD_TARGET_TRIPLE={}", target_triple);
|
||||
@@ -93,5 +94,43 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// tauri_build fails when an externalBin is missing, which is the state of
|
||||
// every `cargo check` or `cargo clippy` that never built the daemon. Drop
|
||||
// the sidecar from the config for those builds so the workspace still
|
||||
// type-checks; a bundle always runs the daemon build first.
|
||||
let sidecar = format!(
|
||||
"{}/target/release/sd-daemon-{}{}",
|
||||
workspace_dir, target_triple, exe_ext
|
||||
);
|
||||
if !std::path::Path::new(&sidecar).exists() {
|
||||
println!(
|
||||
"cargo:warning=sd-daemon sidecar not found in target/release; building without externalBin. Run `bun run daemon:sidecar` before bundling."
|
||||
);
|
||||
let mut config: serde_json::Value = std::env::var("TAURI_CONFIG")
|
||||
.ok()
|
||||
.and_then(|raw| serde_json::from_str(&raw).ok())
|
||||
.unwrap_or_else(|| serde_json::json!({}));
|
||||
config["bundle"]["externalBin"] = serde_json::json!([]);
|
||||
std::env::set_var("TAURI_CONFIG", config.to_string());
|
||||
}
|
||||
|
||||
// generate_context! embeds frontendDist and panics when the directory is
|
||||
// missing, which is the state of a check or clippy run that never built
|
||||
// the frontend. A debug build gets an empty one; a release build still
|
||||
// refuses so a bundle never ships without its UI.
|
||||
let frontend_dist = std::path::Path::new(&manifest_dir).join("../dist");
|
||||
if !frontend_dist.exists() {
|
||||
if profile == "release" {
|
||||
panic!(
|
||||
"{} does not exist; run `bun run build` in apps/tauri before a release build",
|
||||
frontend_dist.display()
|
||||
);
|
||||
}
|
||||
std::fs::create_dir_all(&frontend_dist).expect("create an empty apps/tauri/dist");
|
||||
println!(
|
||||
"cargo:warning=apps/tauri/dist not found; created an empty one so the desktop binary compiles without the frontend. Run `bun run build` in apps/tauri for the real UI."
|
||||
);
|
||||
}
|
||||
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use super::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::windows::SpacedriveWindow;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
|
||||
pub async fn begin_drag(
|
||||
app: AppHandle,
|
||||
config: DragConfig,
|
||||
@@ -101,14 +103,11 @@ pub async fn begin_drag(
|
||||
}
|
||||
|
||||
tracing::info!("Drag started successfully: session_id={}", session_id);
|
||||
Ok(session_id)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
return Err("Drag and drop is only supported on macOS currently".to_string());
|
||||
}
|
||||
|
||||
Ok(session_id)
|
||||
Err("Drag and drop is only supported on macOS currently".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -42,11 +42,3 @@ pub enum DragResult {
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DropEvent {
|
||||
pub window_label: String,
|
||||
pub items: Vec<DragItem>,
|
||||
pub position: (f64, f64),
|
||||
}
|
||||
@@ -18,6 +18,7 @@ pub async fn reveal_file(path: String) -> Result<(), String> {
|
||||
|
||||
/// Share files using the native system share sheet (macOS/iOS only)
|
||||
#[tauri::command]
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
|
||||
pub async fn share_files(paths: Vec<String>) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
/// Keybind registration state
|
||||
/// This tracks registered keybinds for potential use in native menus
|
||||
@@ -18,10 +16,6 @@ impl KeybindState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_accelerator(&self, id: &str) -> Option<String> {
|
||||
self.registered.lock().unwrap().get(id).cloned()
|
||||
}
|
||||
|
||||
pub fn list_all(&self) -> HashMap<String, String> {
|
||||
self.registered.lock().unwrap().clone()
|
||||
}
|
||||
@@ -33,12 +27,6 @@ impl Default for KeybindState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Event payload when a keybind is triggered
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KeybindEvent {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
/// Register a keybind
|
||||
/// This stores the keybind for potential use in native menus
|
||||
/// Actual keyboard handling is done in JavaScript
|
||||
@@ -77,11 +65,3 @@ pub async fn get_registered_keybinds(
|
||||
) -> Result<HashMap<String, String>, String> {
|
||||
Ok(state.list_all())
|
||||
}
|
||||
|
||||
/// Emit a keybind trigger event to the frontend
|
||||
/// This can be called from Rust (e.g., from native menu actions) to trigger keybind handlers
|
||||
pub fn emit_keybind_triggered(app: &AppHandle, id: &str) {
|
||||
if let Err(e) = app.emit("keybind-triggered", KeybindEvent { id: id.to_string() }) {
|
||||
tracing::error!("Failed to emit keybind-triggered event: {}", e);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ mod windows;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tauri::menu::MenuItem;
|
||||
use tauri::Emitter;
|
||||
@@ -454,7 +454,7 @@ async fn validate_and_reset_library_if_needed(
|
||||
app: AppHandle,
|
||||
current_library_id_arc: &Arc<RwLock<Option<String>>>,
|
||||
daemon_state: &Arc<RwLock<DaemonState>>,
|
||||
data_dir: &PathBuf,
|
||||
data_dir: &Path,
|
||||
) -> Result<(), String> {
|
||||
let current_library_id = {
|
||||
let library_id = current_library_id_arc.read().await;
|
||||
@@ -799,7 +799,6 @@ async fn subscribe_to_events(
|
||||
}
|
||||
|
||||
// Explicitly shutdown and drop the stream to close the TCP connection
|
||||
drop(writer);
|
||||
drop(reader);
|
||||
tracing::info!(subscription_id = subscription_id, "TCP connection closed");
|
||||
});
|
||||
@@ -1162,7 +1161,7 @@ WantedBy=default.target
|
||||
|
||||
// Enable and start the service
|
||||
let output = std::process::Command::new("systemctl")
|
||||
.args(&["--user", "daemon-reload"])
|
||||
.args(["--user", "daemon-reload"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to reload systemd: {}", e))?;
|
||||
|
||||
@@ -1172,7 +1171,7 @@ WantedBy=default.target
|
||||
}
|
||||
|
||||
let output = std::process::Command::new("systemctl")
|
||||
.args(&["--user", "enable", "spacedrive-daemon.service"])
|
||||
.args(["--user", "enable", "spacedrive-daemon.service"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to enable service: {}", e))?;
|
||||
|
||||
@@ -1182,7 +1181,7 @@ WantedBy=default.target
|
||||
}
|
||||
|
||||
let output = std::process::Command::new("systemctl")
|
||||
.args(&["--user", "start", "spacedrive-daemon.service"])
|
||||
.args(["--user", "start", "spacedrive-daemon.service"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to start service: {}", e))?;
|
||||
|
||||
@@ -1407,18 +1406,18 @@ async fn uninstall_daemon_service() -> Result<(), String> {
|
||||
if service_path.exists() {
|
||||
// Stop and disable the service
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(&["--user", "stop", "spacedrive-daemon.service"])
|
||||
.args(["--user", "stop", "spacedrive-daemon.service"])
|
||||
.output();
|
||||
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(&["--user", "disable", "spacedrive-daemon.service"])
|
||||
.args(["--user", "disable", "spacedrive-daemon.service"])
|
||||
.output();
|
||||
|
||||
std::fs::remove_file(&service_path)
|
||||
.map_err(|e| format!("Failed to remove service file: {}", e))?;
|
||||
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.args(&["--user", "daemon-reload"])
|
||||
.args(["--user", "daemon-reload"])
|
||||
.output();
|
||||
}
|
||||
|
||||
@@ -1465,14 +1464,11 @@ async fn open_macos_settings() -> Result<(), String> {
|
||||
.arg("x-apple.systempreferences:com.apple.LoginItems-Settings.extension")
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open settings: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
return Err("Not supported on this platform".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Err("Not supported on this platform".to_string())
|
||||
}
|
||||
|
||||
/// Check if daemon is running by trying to connect and send a ping
|
||||
|
||||
@@ -440,6 +440,7 @@ pub async fn close_window(app: AppHandle, label: String) -> Result<(), String> {
|
||||
|
||||
/// Apply macOS window styling to current window (called from frontend when ready)
|
||||
#[tauri::command]
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
|
||||
pub fn apply_macos_styling(app: AppHandle) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
||||
@@ -34,8 +34,10 @@
|
||||
//!
|
||||
//! ## Usage Pattern
|
||||
//! ```rust
|
||||
//! // Set once during initialization
|
||||
//! set_current_device_id(device_manager.device_id()?);
|
||||
//! use sd_core::device::{get_current_device_id, set_current_device_id};
|
||||
//!
|
||||
//! // Set once during initialization, from the device manager's id
|
||||
//! set_current_device_id(uuid::Uuid::new_v4());
|
||||
//!
|
||||
//! // Use anywhere in the codebase
|
||||
//! let device_id = get_current_device_id();
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! ```
|
||||
//! Applications → ApiDispatcher → PermissionLayer → Operations
|
||||
//! ```text
|
||||
//! Applications -> ApiDispatcher -> PermissionLayer -> Operations
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Components
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//!
|
||||
//! ## Example
|
||||
//! ```rust
|
||||
//! use spacedrive_core::infra::source_version::source_version;
|
||||
//! use sd_core::infra::source_version::source_version;
|
||||
//! use std::time::{Duration, UNIX_EPOCH};
|
||||
//!
|
||||
//! let mtime = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
|
||||
|
||||
@@ -39,8 +39,8 @@ impl TimeSource for SystemTimeSource {
|
||||
/// clock skew testing scenarios.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```rust,no_run
|
||||
/// use sd_core::infra::sync::time_source::FakeTimeSource;
|
||||
/// ```rust
|
||||
/// use sd_core::infra::sync::time_source::{FakeTimeSource, TimeSource};
|
||||
///
|
||||
/// let time = FakeTimeSource::new(1000);
|
||||
/// assert_eq!(time.current_time_ms(), 1000);
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
//! mode or cross-volume operations use LocalStreamCopyStrategy for progress tracking.
|
||||
//!
|
||||
//! ## Example
|
||||
//! ```rust,no_run
|
||||
//! use spacedrive_core::ops::files::copy::strategy::{CopyStrategy, LocalMoveStrategy};
|
||||
//! use spacedrive_core::domain::addressing::SdPath;
|
||||
//! Inside a running job, where `ctx` is its `JobContext`:
|
||||
//! ```rust,ignore
|
||||
//! use sd_core::ops::files::copy::strategy::{CopyStrategy, LocalMoveStrategy};
|
||||
//!
|
||||
//! let strategy = LocalMoveStrategy;
|
||||
//! let bytes_moved = strategy.execute(
|
||||
//! &ctx,
|
||||
//! ctx,
|
||||
//! &source_path,
|
||||
//! &dest_path,
|
||||
//! true, // verify_checksum
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
use bytes::Bytes;
|
||||
use sd_core::{
|
||||
domain::addressing::SdPath,
|
||||
ops::files::delete::{routing::DeleteStrategyRouter, strategy::LocalDeleteStrategy},
|
||||
infra::job::types::JobId,
|
||||
ops::files::{
|
||||
delete::{routing::DeleteStrategyRouter, strategy::LocalDeleteStrategy},
|
||||
trash,
|
||||
},
|
||||
volume::backend::{CloudBackend, CloudServiceType, VolumeBackend},
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -82,9 +86,9 @@ async fn test_local_delete_strategy_trash() {
|
||||
|
||||
assert!(test_file.exists());
|
||||
|
||||
// Execute deletion using trash mode
|
||||
let strategy = LocalDeleteStrategy;
|
||||
let result = strategy.move_to_trash(&test_file).await;
|
||||
// LocalDeleteStrategy::move_to_trash needs a running job's context, so
|
||||
// call the trash it delegates to with the same arguments a job supplies.
|
||||
let result = trash::trash(&test_file, None, JobId::new()).await;
|
||||
|
||||
// Verify result - print error if it fails
|
||||
if let Err(e) = &result {
|
||||
@@ -183,7 +187,7 @@ async fn test_delete_modes_all_types() {
|
||||
// Test 2: Trash delete
|
||||
let trash_file = test_root.join("trash.txt");
|
||||
create_test_file(&trash_file, "Trash").await.unwrap();
|
||||
let result = strategy.move_to_trash(&trash_file).await;
|
||||
let result = trash::trash(&trash_file, None, JobId::new()).await;
|
||||
if let Err(e) = &result {
|
||||
eprintln!("Trash test failed with error: {}", e);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ use tokio::{
|
||||
};
|
||||
use tracing::{error, instrument, warn};
|
||||
|
||||
const ONE_MINUTE: Duration = Duration::from_secs(60);
|
||||
const ONE_MINUTE: Duration = Duration::from_mins(1);
|
||||
|
||||
pub trait ActorId: Hash + Eq + Send + Sync + Copy + fmt::Debug + fmt::Display + 'static {}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ impl DiskAccess {
|
||||
///
|
||||
/// Once ran, it will open the "Full Disk Access" prompt.
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
#[must_use]
|
||||
pub fn request_fda() -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn extract_audio_samples(filename: impl AsRef<Path>) -> Result<Vec<f32>, Err
|
||||
find_best_audio_stream(format_ctx.as_ref()).ok_or(FFmpegError::StreamNotFound)?;
|
||||
|
||||
let audio_stream = format_ctx
|
||||
.stream(audio_stream_index as u32)
|
||||
.stream(u32::try_from(audio_stream_index).map_err(|_| FFmpegError::StreamNotFound)?)
|
||||
.ok_or(FFmpegError::StreamNotFound)?;
|
||||
|
||||
// Get codec parameters
|
||||
@@ -90,7 +90,13 @@ pub fn extract_audio_samples(filename: impl AsRef<Path>) -> Result<Vec<f32>, Err
|
||||
let in_channels = codec_ref.ch_layout.nb_channels;
|
||||
|
||||
let final_samples = if in_sample_rate != 16000 || in_channels != 1 {
|
||||
resample_audio(&samples, in_sample_rate, in_channels, 16000, 1)?
|
||||
let in_sample_rate =
|
||||
usize::try_from(in_sample_rate).map_err(|_| FFmpegError::InvalidData)?;
|
||||
let in_channels = usize::try_from(in_channels).map_err(|_| FFmpegError::InvalidData)?;
|
||||
if in_sample_rate == 0 || in_channels == 0 {
|
||||
return Err(FFmpegError::InvalidData.into());
|
||||
}
|
||||
resample_audio(&samples, in_sample_rate, in_channels, 16000, 1)
|
||||
} else {
|
||||
samples
|
||||
};
|
||||
@@ -106,21 +112,26 @@ unsafe fn find_best_audio_stream(format_ctx: &ffmpeg_sys_next::AVFormatContext)
|
||||
return None;
|
||||
}
|
||||
|
||||
for i in 0..format_ctx.nb_streams {
|
||||
let stream = (*streams.add(i as usize)).as_ref()?;
|
||||
for i in 0..usize::try_from(format_ctx.nb_streams).ok()? {
|
||||
let stream = (*streams.add(i)).as_ref()?;
|
||||
let codecpar = stream.codecpar.as_ref()?;
|
||||
|
||||
if codecpar.codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO {
|
||||
return Some(i as i32);
|
||||
return i32::try_from(i).ok();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract and convert audio frame to f32 samples
|
||||
// FFmpeg allocates frame planes with at least 32-byte alignment, so viewing
|
||||
// the byte planes as their sample type is sound. s32 samples deliberately lose
|
||||
// their low bits when they become f32, which is the output format.
|
||||
#[allow(clippy::cast_ptr_alignment, clippy::cast_precision_loss)]
|
||||
unsafe fn extract_and_convert_frame(frame: &AVFrame) -> Result<Vec<f32>, Error> {
|
||||
let nb_samples = frame.nb_samples as usize;
|
||||
let channels = frame.ch_layout.nb_channels as usize;
|
||||
let nb_samples = usize::try_from(frame.nb_samples).map_err(|_| FFmpegError::InvalidData)?;
|
||||
let channels =
|
||||
usize::try_from(frame.ch_layout.nb_channels).map_err(|_| FFmpegError::InvalidData)?;
|
||||
let format = frame.format;
|
||||
|
||||
match format {
|
||||
@@ -144,7 +155,7 @@ unsafe fn extract_and_convert_frame(frame: &AVFrame) -> Result<Vec<f32>, Error>
|
||||
f if f == AVSampleFormat::AV_SAMPLE_FMT_S16 as i32 => {
|
||||
// Interleaved s16 - convert to f32
|
||||
let data = slice::from_raw_parts(frame.data[0] as *const i16, nb_samples * channels);
|
||||
Ok(data.iter().map(|&s| s as f32 / 32768.0).collect())
|
||||
Ok(data.iter().map(|&s| f32::from(s) / 32768.0).collect())
|
||||
}
|
||||
f if f == AVSampleFormat::AV_SAMPLE_FMT_S16P as i32 => {
|
||||
// Planar s16 - interleave and convert
|
||||
@@ -153,7 +164,7 @@ unsafe fn extract_and_convert_frame(frame: &AVFrame) -> Result<Vec<f32>, Error>
|
||||
for ch in 0..channels {
|
||||
let channel_data =
|
||||
slice::from_raw_parts(frame.data[ch] as *const i16, nb_samples);
|
||||
output.push(channel_data[i] as f32 / 32768.0);
|
||||
output.push(f32::from(channel_data[i]) / 32768.0);
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
@@ -161,7 +172,7 @@ unsafe fn extract_and_convert_frame(frame: &AVFrame) -> Result<Vec<f32>, Error>
|
||||
f if f == AVSampleFormat::AV_SAMPLE_FMT_S32 as i32 => {
|
||||
// Interleaved s32 - convert to f32
|
||||
let data = slice::from_raw_parts(frame.data[0] as *const i32, nb_samples * channels);
|
||||
Ok(data.iter().map(|&s| s as f32 / 2147483648.0).collect())
|
||||
Ok(data.iter().map(|&s| s as f32 / 2_147_483_648.0).collect())
|
||||
}
|
||||
f if f == AVSampleFormat::AV_SAMPLE_FMT_S32P as i32 => {
|
||||
// Planar s32 - interleave and convert
|
||||
@@ -170,7 +181,7 @@ unsafe fn extract_and_convert_frame(frame: &AVFrame) -> Result<Vec<f32>, Error>
|
||||
for ch in 0..channels {
|
||||
let channel_data =
|
||||
slice::from_raw_parts(frame.data[ch] as *const i32, nb_samples);
|
||||
output.push(channel_data[i] as f32 / 2147483648.0);
|
||||
output.push(channel_data[i] as f32 / 2_147_483_648.0);
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
@@ -179,84 +190,79 @@ unsafe fn extract_and_convert_frame(frame: &AVFrame) -> Result<Vec<f32>, Error>
|
||||
}
|
||||
}
|
||||
|
||||
/// Linear interpolation between two neighbouring samples.
|
||||
fn lerp(s1: f32, s2: f32, frac: f32) -> f32 {
|
||||
s1.mul_add(1.0 - frac, s2 * frac)
|
||||
}
|
||||
|
||||
/// Simple resampling using linear interpolation
|
||||
/// For production, this should use a proper resampling library
|
||||
// The fractional position is computed from a remainder below `out_rate` and
|
||||
// the downmix count is below `in_channels`, both far under 2^24 where f32 is
|
||||
// exact.
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn resample_audio(
|
||||
samples: &[f32],
|
||||
in_rate: i32,
|
||||
in_channels: i32,
|
||||
out_rate: i32,
|
||||
out_channels: i32,
|
||||
) -> Result<Vec<f32>, Error> {
|
||||
in_rate: usize,
|
||||
in_channels: usize,
|
||||
out_rate: usize,
|
||||
out_channels: usize,
|
||||
) -> Vec<f32> {
|
||||
if samples.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let in_rate = in_rate as usize;
|
||||
let out_rate = out_rate as usize;
|
||||
let in_channels = in_channels as usize;
|
||||
let out_channels = out_channels as usize;
|
||||
|
||||
let in_frames = samples.len() / in_channels;
|
||||
let out_frames = (in_frames * out_rate + in_rate - 1) / in_rate;
|
||||
let out_frames = (in_frames * out_rate).div_ceil(in_rate);
|
||||
|
||||
let mut output = Vec::with_capacity(out_frames * out_channels);
|
||||
|
||||
for out_frame_idx in 0..out_frames {
|
||||
// Calculate corresponding input frame (with fractional part)
|
||||
let in_frame_pos = (out_frame_idx * in_rate) as f32 / out_rate as f32;
|
||||
let in_frame_idx = in_frame_pos as usize;
|
||||
let frac = in_frame_pos - in_frame_idx as f32;
|
||||
// Corresponding input frame, split into whole frame and fraction
|
||||
let in_pos = out_frame_idx * in_rate;
|
||||
let in_frame_idx = in_pos / out_rate;
|
||||
let frac = (in_pos % out_rate) as f32 / out_rate as f32;
|
||||
|
||||
// For each output channel
|
||||
for out_ch in 0..out_channels {
|
||||
let mut sample = 0.0f32;
|
||||
|
||||
if in_channels == out_channels {
|
||||
// Same channel count - just resample
|
||||
let in_ch = out_ch;
|
||||
|
||||
let sample_at = |in_ch: usize| {
|
||||
if in_frame_idx + 1 < in_frames {
|
||||
let s1 = samples[in_frame_idx * in_channels + in_ch];
|
||||
let s2 = samples[(in_frame_idx + 1) * in_channels + in_ch];
|
||||
sample = s1 * (1.0 - frac) + s2 * frac;
|
||||
Some(lerp(s1, s2, frac))
|
||||
} else if in_frame_idx < in_frames {
|
||||
sample = samples[in_frame_idx * in_channels + in_ch];
|
||||
Some(samples[in_frame_idx * in_channels + in_ch])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if in_channels > out_channels {
|
||||
// Downmix (e.g., stereo to mono) - average channels
|
||||
let mut sum = 0.0f32;
|
||||
let mut count = 0;
|
||||
};
|
||||
|
||||
for in_ch in 0..in_channels {
|
||||
if in_frame_idx + 1 < in_frames {
|
||||
let s1 = samples[in_frame_idx * in_channels + in_ch];
|
||||
let s2 = samples[(in_frame_idx + 1) * in_channels + in_ch];
|
||||
sum += s1 * (1.0 - frac) + s2 * frac;
|
||||
count += 1;
|
||||
} else if in_frame_idx < in_frames {
|
||||
sum += samples[in_frame_idx * in_channels + in_ch];
|
||||
count += 1;
|
||||
let sample = match in_channels.cmp(&out_channels) {
|
||||
// Same channel count - just resample
|
||||
std::cmp::Ordering::Equal => sample_at(out_ch).unwrap_or(0.0),
|
||||
// Downmix (e.g., stereo to mono) - average channels
|
||||
std::cmp::Ordering::Greater => {
|
||||
let mut sum = 0.0f32;
|
||||
let mut count = 0usize;
|
||||
for in_ch in 0..in_channels {
|
||||
if let Some(s) = sample_at(in_ch) {
|
||||
sum += s;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if count > 0 {
|
||||
sum / count as f32
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
sample = if count > 0 { sum / count as f32 } else { 0.0 };
|
||||
} else {
|
||||
// Upmix (e.g., mono to stereo) - duplicate channel
|
||||
let in_ch = 0;
|
||||
|
||||
if in_frame_idx + 1 < in_frames {
|
||||
let s1 = samples[in_frame_idx * in_channels + in_ch];
|
||||
let s2 = samples[(in_frame_idx + 1) * in_channels + in_ch];
|
||||
sample = s1 * (1.0 - frac) + s2 * frac;
|
||||
} else if in_frame_idx < in_frames {
|
||||
sample = samples[in_frame_idx * in_channels + in_ch];
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Less => sample_at(0).unwrap_or(0.0),
|
||||
};
|
||||
|
||||
output.push(sample);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
output
|
||||
}
|
||||
@@ -36,11 +36,11 @@ impl FFmpegCodecContext {
|
||||
Ok(Self(ptr))
|
||||
}
|
||||
|
||||
pub(crate) fn as_ref(&self) -> &AVCodecContext {
|
||||
pub(crate) const fn as_ref(&self) -> &AVCodecContext {
|
||||
unsafe { self.0.as_ref() }.expect("initialized on struct creation")
|
||||
}
|
||||
|
||||
pub(crate) fn as_mut(&mut self) -> &mut AVCodecContext {
|
||||
pub(crate) const fn as_mut(&mut self) -> &mut AVCodecContext {
|
||||
unsafe { self.0.as_mut() }.expect("initialized on struct creation")
|
||||
}
|
||||
|
||||
@@ -248,19 +248,24 @@ impl FFmpegCodecContext {
|
||||
reserved_padding: [0; 1000],
|
||||
};
|
||||
unsafe {
|
||||
av_bprint_init(&mut bprint, 0, u32::MAX /* AV_BPRINT_SIZE_UNLIMITED */);
|
||||
av_bprint_init(
|
||||
&raw mut bprint,
|
||||
0,
|
||||
u32::MAX, /* AV_BPRINT_SIZE_UNLIMITED */
|
||||
);
|
||||
};
|
||||
let mut channel_layout = ptr::null_mut();
|
||||
let channel_layout =
|
||||
if unsafe { av_channel_layout_describe_bprint(&ctx.ch_layout, &mut bprint) } < 0
|
||||
|| unsafe { av_bprint_finalize(&mut bprint, &mut channel_layout) } < 0
|
||||
|| channel_layout.is_null()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
let cstr = unsafe { CStr::from_ptr(channel_layout) };
|
||||
Some(String::from_utf8_lossy(cstr.to_bytes()).to_string())
|
||||
};
|
||||
let channel_layout = if unsafe {
|
||||
av_channel_layout_describe_bprint(&raw const ctx.ch_layout, &raw mut bprint)
|
||||
} < 0 || unsafe {
|
||||
av_bprint_finalize(&raw mut bprint, &raw mut channel_layout)
|
||||
} < 0 || channel_layout.is_null()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
let cstr = unsafe { CStr::from_ptr(channel_layout) };
|
||||
Some(String::from_utf8_lossy(cstr.to_bytes()).to_string())
|
||||
};
|
||||
|
||||
let sample_format = if ctx.sample_fmt == AVSampleFormat::AV_SAMPLE_FMT_NONE {
|
||||
None
|
||||
@@ -324,8 +329,8 @@ fn extract_aspect_ratio(
|
||||
let max = 1024 * 1024;
|
||||
unsafe {
|
||||
av_reduce(
|
||||
&mut display_aspect_ratio.num,
|
||||
&mut display_aspect_ratio.den,
|
||||
&raw mut display_aspect_ratio.num,
|
||||
&raw mut display_aspect_ratio.den,
|
||||
num,
|
||||
den,
|
||||
max,
|
||||
@@ -437,7 +442,7 @@ fn extract_pixel_format(ctx: &AVCodecContext) -> Option<String> {
|
||||
impl Drop for FFmpegCodecContext {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() {
|
||||
unsafe { avcodec_free_context(&mut self.0) };
|
||||
unsafe { avcodec_free_context(&raw mut self.0) };
|
||||
self.0 = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ impl FFmpegDictionary {
|
||||
check_error(
|
||||
unsafe {
|
||||
av_dict_set(
|
||||
&mut self.dict,
|
||||
&raw mut self.dict,
|
||||
key.as_ptr(),
|
||||
ptr::null(),
|
||||
AV_DICT_MATCH_CASE,
|
||||
@@ -61,7 +61,7 @@ impl FFmpegDictionary {
|
||||
impl Drop for FFmpegDictionary {
|
||||
fn drop(&mut self) {
|
||||
if self.managed && !self.dict.is_null() {
|
||||
unsafe { av_dict_free(&mut self.dict) };
|
||||
unsafe { av_dict_free(&raw mut self.dict) };
|
||||
self.dict = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ impl<'a> FFmpegFilterGraph {
|
||||
|
||||
let mut filter_source = ptr::null_mut();
|
||||
filter_graph.setup_filter(
|
||||
&mut filter_source,
|
||||
&raw mut filter_source,
|
||||
c"buffer",
|
||||
c"thumb_buffer",
|
||||
Some(CString::new(args)?.as_c_str()),
|
||||
@@ -70,7 +70,7 @@ impl<'a> FFmpegFilterGraph {
|
||||
|
||||
let mut filter_sink = ptr::null_mut();
|
||||
filter_graph.setup_filter(
|
||||
&mut filter_sink,
|
||||
&raw mut filter_sink,
|
||||
c"buffersink",
|
||||
c"thumb_buffersink",
|
||||
None,
|
||||
@@ -81,7 +81,7 @@ impl<'a> FFmpegFilterGraph {
|
||||
let mut yadif_filter = ptr::null_mut();
|
||||
if interlaced_frame {
|
||||
filter_graph.setup_filter(
|
||||
&mut yadif_filter,
|
||||
&raw mut yadif_filter,
|
||||
c"yadif",
|
||||
c"thumb_deint",
|
||||
Some(c"deint=1"),
|
||||
@@ -91,7 +91,7 @@ impl<'a> FFmpegFilterGraph {
|
||||
|
||||
let mut scale_filter = ptr::null_mut();
|
||||
filter_graph.setup_filter(
|
||||
&mut scale_filter,
|
||||
&raw mut scale_filter,
|
||||
c"scale",
|
||||
c"thumb_scale",
|
||||
Some(
|
||||
@@ -108,7 +108,7 @@ impl<'a> FFmpegFilterGraph {
|
||||
|
||||
let mut format_filter = ptr::null_mut();
|
||||
filter_graph.setup_filter(
|
||||
&mut format_filter,
|
||||
&raw mut format_filter,
|
||||
c"format",
|
||||
c"thumb_format",
|
||||
Some(c"pix_fmts=rgb24"),
|
||||
@@ -158,7 +158,7 @@ impl<'a> FFmpegFilterGraph {
|
||||
Ok((filter_graph, filter_source_ctx, filter_sink_ctx))
|
||||
}
|
||||
|
||||
pub(crate) fn as_mut(&mut self) -> &mut AVFilterGraph {
|
||||
pub(crate) const fn as_mut(&mut self) -> &mut AVFilterGraph {
|
||||
unsafe { self.0.as_mut() }.expect("initialized on struct creation")
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ impl<'a> FFmpegFilterGraph {
|
||||
impl Drop for FFmpegFilterGraph {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() {
|
||||
unsafe { avfilter_graph_free(&mut self.0) };
|
||||
unsafe { avfilter_graph_free(&raw mut self.0) };
|
||||
self.0 = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,22 +42,27 @@ impl FFmpegFormatContext {
|
||||
|
||||
check_error(
|
||||
unsafe {
|
||||
avformat_open_input(&mut ptr, filename.as_ptr(), ptr::null(), ptr::null_mut())
|
||||
avformat_open_input(
|
||||
&raw mut ptr,
|
||||
filename.as_ptr(),
|
||||
ptr::null(),
|
||||
ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
"Fail to open an input stream and read the header",
|
||||
)
|
||||
.map(|()| Self(ptr))
|
||||
}
|
||||
|
||||
pub(crate) fn as_ref(&self) -> &AVFormatContext {
|
||||
pub(crate) const fn as_ref(&self) -> &AVFormatContext {
|
||||
unsafe { self.0.as_ref() }.expect("initialized on struct creation")
|
||||
}
|
||||
|
||||
pub(crate) fn as_mut(&mut self) -> &mut AVFormatContext {
|
||||
pub(crate) const fn as_mut(&mut self) -> &mut AVFormatContext {
|
||||
unsafe { self.0.as_mut() }.expect("initialized on struct creation")
|
||||
}
|
||||
|
||||
pub(crate) fn duration(&self) -> Option<i64> {
|
||||
pub(crate) const fn duration(&self) -> Option<i64> {
|
||||
let duration = self.as_ref().duration;
|
||||
if duration == AV_NOPTS_VALUE {
|
||||
return None;
|
||||
@@ -207,7 +212,7 @@ impl FFmpegFormatContext {
|
||||
.unwrap_or(vec![])
|
||||
}
|
||||
|
||||
fn start_time(&self) -> Option<i64> {
|
||||
const fn start_time(&self) -> Option<i64> {
|
||||
let start_time = self.as_ref().start_time;
|
||||
if start_time == AV_NOPTS_VALUE {
|
||||
return None;
|
||||
@@ -216,7 +221,7 @@ impl FFmpegFormatContext {
|
||||
Some(start_time)
|
||||
}
|
||||
|
||||
fn bit_rate(&self) -> i64 {
|
||||
const fn bit_rate(&self) -> i64 {
|
||||
self.as_ref().bit_rate
|
||||
}
|
||||
|
||||
@@ -296,7 +301,7 @@ impl FFmpegFormatContext {
|
||||
impl Drop for FFmpegFormatContext {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() {
|
||||
unsafe { avformat_close_input(&mut self.0) };
|
||||
unsafe { avformat_close_input(&raw mut self.0) };
|
||||
self.0 = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
@@ -357,8 +362,8 @@ impl From<&AVStream> for FFmpegStream {
|
||||
let max = 1024 * 1024;
|
||||
unsafe {
|
||||
av_reduce(
|
||||
&mut display_aspect_ratio.num,
|
||||
&mut display_aspect_ratio.den,
|
||||
&raw mut display_aspect_ratio.num,
|
||||
&raw mut display_aspect_ratio.den,
|
||||
num,
|
||||
den,
|
||||
max,
|
||||
|
||||
@@ -304,7 +304,7 @@ impl FrameDecoder {
|
||||
impl Drop for FrameDecoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
av_packet_free(&mut self.packet);
|
||||
av_packet_free(&raw mut self.packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,11 @@ impl FFmpegPacket {
|
||||
Ok(Self(ptr))
|
||||
}
|
||||
|
||||
pub(crate) fn as_ptr(&self) -> *mut AVPacket {
|
||||
pub(crate) const fn as_ptr(&self) -> *mut AVPacket {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub(crate) fn as_ref(&self) -> Option<&AVPacket> {
|
||||
pub(crate) const fn as_ref(&self) -> Option<&AVPacket> {
|
||||
unsafe { self.0.as_ref() }
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl FFmpegPacket {
|
||||
impl Drop for FFmpegPacket {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() {
|
||||
unsafe { av_packet_free(&mut self.0) };
|
||||
unsafe { av_packet_free(&raw mut self.0) };
|
||||
self.0 = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ impl FFmpegFrame {
|
||||
Ok(Self(ptr))
|
||||
}
|
||||
|
||||
pub(crate) fn as_ref(&self) -> &AVFrame {
|
||||
pub(crate) const fn as_ref(&self) -> &AVFrame {
|
||||
unsafe { self.0.as_ref() }.expect("initialized on struct creation")
|
||||
}
|
||||
|
||||
pub(crate) fn as_mut(&mut self) -> &mut AVFrame {
|
||||
pub(crate) const fn as_mut(&mut self) -> &mut AVFrame {
|
||||
unsafe { self.0.as_mut() }.expect("initialized on struct creation")
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ impl FFmpegFrame {
|
||||
impl Drop for FFmpegFrame {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() {
|
||||
unsafe { av_frame_free(&mut self.0) };
|
||||
unsafe { av_frame_free(&raw mut self.0) };
|
||||
self.0 = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@ use syn::{parse_macro_input, Data, DataStruct, DeriveInput};
|
||||
/// This macro generates the necessary code to automatically register a job type
|
||||
/// with the job registry using the `inventory` crate.
|
||||
///
|
||||
/// The expansion refers to `crate::infra` and `crate::library`, so the
|
||||
/// derive only compiles inside `sd-core`; the example is not compiled here.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```rust
|
||||
/// ```ignore
|
||||
/// use job_derive::Job;
|
||||
///
|
||||
/// #[derive(Job, Serialize, Deserialize)]
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn generate_markdown_report(
|
||||
report.push_str("## Top Templates by Frequency\n\n");
|
||||
|
||||
let mut sorted_templates = templates.to_vec();
|
||||
sorted_templates.sort_by(|a, b| b.total_count.cmp(&a.total_count));
|
||||
sorted_templates.sort_by_key(|t| std::cmp::Reverse(t.total_count));
|
||||
|
||||
for (i, template) in sorted_templates.iter().take(10).enumerate() {
|
||||
report.push_str(&format!("### {}. Template #{}\n\n", i + 1, template.id));
|
||||
@@ -73,7 +73,7 @@ pub fn generate_markdown_report(
|
||||
report.push_str("## Top Collapsed Groups\n\n");
|
||||
|
||||
let mut sorted_groups = groups.to_vec();
|
||||
sorted_groups.sort_by(|a, b| b.count.cmp(&a.count));
|
||||
sorted_groups.sort_by_key(|g| std::cmp::Reverse(g.count));
|
||||
|
||||
for (i, group) in sorted_groups.iter().take(10).enumerate() {
|
||||
let template = templates.iter().find(|t| t.id == group.template_id);
|
||||
@@ -111,7 +111,7 @@ pub fn generate_markdown_report(
|
||||
report.push_str("Repeating patterns of log groups:\n\n");
|
||||
|
||||
let mut sorted_sequences = sequences.to_vec();
|
||||
sorted_sequences.sort_by(|a, b| b.repetitions.cmp(&a.repetitions));
|
||||
sorted_sequences.sort_by_key(|s| std::cmp::Reverse(s.repetitions));
|
||||
|
||||
for (i, seq) in sorted_sequences.iter().take(10).enumerate() {
|
||||
report.push_str(&format!(
|
||||
|
||||
@@ -5,7 +5,7 @@ use exif::Tag;
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::DMS_DIVISION;
|
||||
/// use sd_media_metadata::exif::DMS_DIVISION;
|
||||
///
|
||||
/// let latitude = [53.0, 19.0, 35.11]; // in DMS
|
||||
/// latitude.iter().zip(DMS_DIVISION.iter());
|
||||
|
||||
@@ -28,7 +28,7 @@ impl MediaLocation {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::MediaLocation;
|
||||
/// use sd_media_metadata::exif::MediaLocation;
|
||||
///
|
||||
/// let x = MediaLocation::new(38.89767633, -7.36560353, Some(32), Some(20));
|
||||
/// ```
|
||||
@@ -58,7 +58,7 @@ impl MediaLocation {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// use sd_media_metadata::image::{ExifReader, Location};
|
||||
/// use sd_media_metadata::exif::{ExifReader, Location};
|
||||
///
|
||||
/// let mut reader = ExifReader::from_path("path").unwrap();
|
||||
/// MediaLocation::from_exif_reader(&mut reader).unwrap();
|
||||
@@ -111,7 +111,7 @@ impl MediaLocation {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::MediaLocation;
|
||||
/// use sd_media_metadata::exif::MediaLocation;
|
||||
///
|
||||
/// let mut home = MediaLocation::new(38.89767633, -7.36560353, Some(32), Some(20));
|
||||
/// assert_eq!(home.coordinates(), (38.89767633, -7.36560353));
|
||||
@@ -127,10 +127,10 @@ impl MediaLocation {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::MediaLocation;
|
||||
/// use sd_media_metadata::exif::MediaLocation;
|
||||
///
|
||||
/// let mut home = MediaLocation::new(38.89767633, -7.36560353, Some(32), Some(20));
|
||||
/// assert_eq!(home.pluscode().to_string(), "894HFGG5+82".to_string());
|
||||
/// assert_eq!(home.pluscode().to_string(), "8CCJVJXM+3Q".to_string());
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
@@ -143,7 +143,7 @@ impl MediaLocation {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::MediaLocation;
|
||||
/// use sd_media_metadata::exif::MediaLocation;
|
||||
///
|
||||
/// let mut home = MediaLocation::new(38.89767633, -7.36560353, Some(32), Some(20));
|
||||
/// home.update_latitude(60_f64);
|
||||
@@ -159,7 +159,7 @@ impl MediaLocation {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::MediaLocation;
|
||||
/// use sd_media_metadata::exif::MediaLocation;
|
||||
///
|
||||
/// let mut home = MediaLocation::new(38.89767633, -7.36560353, Some(32), Some(20));
|
||||
/// home.update_longitude(20_f64);
|
||||
@@ -173,7 +173,7 @@ impl MediaLocation {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::MediaLocation;
|
||||
/// use sd_media_metadata::exif::MediaLocation;
|
||||
///
|
||||
/// let mut home = MediaLocation::new(38.89767633, -7.36560353, Some(32), Some(20));
|
||||
/// home.update_altitude(20);
|
||||
@@ -186,7 +186,7 @@ impl MediaLocation {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::MediaLocation;
|
||||
/// use sd_media_metadata::exif::MediaLocation;
|
||||
///
|
||||
/// let mut home = MediaLocation::new(38.89767633, -7.36560353, Some(32), Some(20));
|
||||
/// home.update_direction(233);
|
||||
@@ -228,11 +228,11 @@ impl TryFrom<String> for MediaLocation {
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::MediaLocation;
|
||||
/// use sd_media_metadata::exif::MediaLocation;
|
||||
///
|
||||
/// let s = String::from("32.47583923, -28.49238495");
|
||||
/// let location = MediaLocation::try_from(s).unwrap();
|
||||
/// assert_eq!(location.to_string(), "32.47583923, -28.49238495".to_string());
|
||||
/// assert_eq!(location.coordinates(), (32.47583923, -28.49238495));
|
||||
/// ```
|
||||
fn try_from(mut value: String) -> std::result::Result<Self, Self::Error> {
|
||||
value.retain(|c| !c.is_whitespace() || c.is_numeric() || c == '-' || c == '.');
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Resolution {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use sd_media_metadata::image::Resolution;
|
||||
/// use sd_media_metadata::exif::Resolution;
|
||||
///
|
||||
/// Resolution::new(1920, 1080);
|
||||
/// ```
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! This sketches the intended API. `ExtensionContext`, `CreateEntry` and
|
||||
//! `OcrOptions` do not exist yet, so the example is not compiled.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use spacedrive_sdk::{ExtensionContext, prelude::*};
|
||||
//!
|
||||
//! #[spacedrive_extension]
|
||||
|
||||
+39
-35
@@ -39,6 +39,33 @@ use crate::schema::{
|
||||
DataTypeMeta, DataTypeSchema, FieldType, ModelDef, RelationsDef, SearchContract,
|
||||
};
|
||||
|
||||
/// One `record JOIN facet_file` row: uuid, parent uuid, title, size, mtime, inode.
|
||||
type LedgerRow = (Uuid, Option<Uuid>, Option<String>, i64, i64, Option<i64>);
|
||||
|
||||
/// One pending-content row: uuid, parent uuid, parent path, title, size.
|
||||
type PendingContentRow = (Uuid, Option<Uuid>, Option<String>, Option<String>, i64);
|
||||
|
||||
/// A pending-content row plus the sampled hash to verify against.
|
||||
type PendingVerificationRow = (
|
||||
Uuid,
|
||||
Option<Uuid>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
i64,
|
||||
Option<String>,
|
||||
);
|
||||
|
||||
/// One copy of a content row: content uuid, size, record uuid, parent uuid,
|
||||
/// parent path, title.
|
||||
type ContentCopyRow = (
|
||||
Uuid,
|
||||
Option<i64>,
|
||||
Uuid,
|
||||
Option<Uuid>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
);
|
||||
|
||||
/// What the walk found at a path. `record.type` carries this, so a directory
|
||||
/// is queryable without joining a facet.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -197,13 +224,12 @@ impl Ledger {
|
||||
.map(|(uuid, path)| (uuid, Arc::from(path.as_str())))
|
||||
.collect();
|
||||
|
||||
let rows: Vec<(Uuid, Option<Uuid>, Option<String>, i64, i64, Option<i64>)> =
|
||||
sqlx::query_as(
|
||||
"SELECT r.uuid, r.parent_uuid, r.title, f.size, f.mtime, f.inode
|
||||
let rows: Vec<LedgerRow> = sqlx::query_as(
|
||||
"SELECT r.uuid, r.parent_uuid, r.title, f.size, f.mtime, f.inode
|
||||
FROM record r JOIN facet_file f ON f.record_uuid = r.uuid",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut ledger = Self::default();
|
||||
for (uuid, parent_uuid, title, size, mtime, inode) in rows {
|
||||
@@ -759,13 +785,12 @@ pub async fn files_needing_content(
|
||||
pool: &sqlx::SqlitePool,
|
||||
batch_size: usize,
|
||||
) -> Result<Vec<PendingContent>> {
|
||||
let rows: Vec<(Uuid, Option<Uuid>, Option<String>, Option<String>, i64)> =
|
||||
sqlx::query_as(&format!(
|
||||
let rows: Vec<PendingContentRow> = sqlx::query_as(&format!(
|
||||
"SELECT r.uuid, r.parent_uuid, d.path, r.title, f.size {PENDING_CONTENT} ORDER BY r.rowid LIMIT ?"
|
||||
))
|
||||
.bind(batch_size as i64)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
.bind(batch_size as i64)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
@@ -821,14 +846,7 @@ pub async fn files_needing_verification(
|
||||
pool: &sqlx::SqlitePool,
|
||||
batch_size: usize,
|
||||
) -> Result<Vec<PendingVerification>> {
|
||||
let rows: Vec<(
|
||||
Uuid,
|
||||
Option<Uuid>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
i64,
|
||||
Option<String>,
|
||||
)> = sqlx::query_as(&format!(
|
||||
let rows: Vec<PendingVerificationRow> = sqlx::query_as(&format!(
|
||||
"SELECT r.uuid, r.parent_uuid, d.path, r.title, f.size, c.sampled_hash \
|
||||
{PENDING_VERIFICATION} ORDER BY r.rowid LIMIT ?"
|
||||
))
|
||||
@@ -897,14 +915,7 @@ pub async fn duplicate_copies(
|
||||
min_size: i64,
|
||||
group_limit: usize,
|
||||
) -> Result<Vec<ContentCopy>> {
|
||||
let rows: Vec<(
|
||||
Uuid,
|
||||
Option<i64>,
|
||||
Uuid,
|
||||
Option<Uuid>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)> = sqlx::query_as(
|
||||
let rows: Vec<ContentCopyRow> = sqlx::query_as(
|
||||
"WITH duplicated AS (
|
||||
SELECT r.content_id AS content_id, c.size AS size
|
||||
FROM record r
|
||||
@@ -966,14 +977,7 @@ pub async fn copies_of_content(
|
||||
pool: &sqlx::SqlitePool,
|
||||
content_uuid: Uuid,
|
||||
) -> Result<Vec<ContentCopy>> {
|
||||
let rows: Vec<(
|
||||
Uuid,
|
||||
Option<i64>,
|
||||
Uuid,
|
||||
Option<Uuid>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)> = sqlx::query_as(
|
||||
let rows: Vec<ContentCopyRow> = sqlx::query_as(
|
||||
"SELECT c.uuid, c.size, r.uuid, r.parent_uuid, d.path, r.title
|
||||
FROM content c
|
||||
JOIN record r ON r.content_id = c.id
|
||||
|
||||
@@ -47,11 +47,14 @@
|
||||
//! async fn main() {
|
||||
//! let system = TaskSystem::new();
|
||||
//!
|
||||
//! let handle = system.dispatch(ReadyTask { id: TaskId::new_v4() }).await;
|
||||
//! let handle = system
|
||||
//! .dispatch(ReadyTask { id: TaskId::new_v4() })
|
||||
//! .await
|
||||
//! .expect("the system is running");
|
||||
//!
|
||||
//! assert!(matches!(
|
||||
//! handle.await,
|
||||
//! Ok(TaskStatus::Done(TaskOutput::Empty))
|
||||
//! Ok(TaskStatus::Done((_, TaskOutput::Empty)))
|
||||
//! ));
|
||||
//!
|
||||
//! system.shutdown().await;
|
||||
|
||||
@@ -931,12 +931,11 @@ impl<E: RunError> Drop for PanicOnSenderDrop<E> {
|
||||
// that took the whole process down, so the invariant stays loud
|
||||
// in debug builds and becomes a log in release.
|
||||
#[cfg(debug_assertions)]
|
||||
if !std::thread::panicking() {
|
||||
panic!(
|
||||
"TaskHandle done channel dropped before sending a result: {}",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
std::thread::panicking(),
|
||||
"TaskHandle done channel dropped before sending a result: {}",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
#[cfg(not(debug_assertions))]
|
||||
tracing::warn!(
|
||||
task_id = %self.task_id,
|
||||
|
||||
@@ -36,7 +36,7 @@ use super::{
|
||||
};
|
||||
|
||||
const TEN_SECONDS: Duration = Duration::from_secs(10);
|
||||
const ONE_MINUTE: Duration = Duration::from_secs(60);
|
||||
const ONE_MINUTE: Duration = Duration::from_mins(1);
|
||||
|
||||
const TASK_QUEUE_INITIAL_SIZE: usize = 64;
|
||||
const PRIORITY_TASK_QUEUE_INITIAL_SIZE: usize = 32;
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
"eslintConfig": {
|
||||
"root": true
|
||||
},
|
||||
"packageManager": "bun@1.3.0",
|
||||
"packageManager": "bun@1.3.4",
|
||||
"overrides": {
|
||||
"@types/node": ">18.18.x",
|
||||
"@types/react": "19.2.14",
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import {
|
||||
ArrowsClockwise,
|
||||
Calendar,
|
||||
ChatCircle,
|
||||
ClockCounterClockwise,
|
||||
Cube,
|
||||
DotsThree,
|
||||
Eye,
|
||||
FilmStrip,
|
||||
Fingerprint,
|
||||
HardDrive,
|
||||
Hash,
|
||||
@@ -15,7 +12,6 @@ import {
|
||||
Info,
|
||||
MagnifyingGlass,
|
||||
MapPin,
|
||||
Microphone,
|
||||
Paperclip,
|
||||
PaperPlaneRight,
|
||||
ShareNetwork,
|
||||
@@ -23,8 +19,7 @@ import {
|
||||
Tag as TagIcon,
|
||||
TextAa,
|
||||
Timer,
|
||||
Trash,
|
||||
VideoCamera
|
||||
Trash
|
||||
} from '@phosphor-icons/react';
|
||||
import {getIcon} from '@sd/assets/util';
|
||||
import type {File, SdPath} from '@sd/ts-client';
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import type { SourceInfo } from "@sd/ts-client";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAdapterIcons } from "../../hooks/useAdapterIcons";
|
||||
import { SourceTypeIcon } from "./SourceTypeIcon";
|
||||
import { SourceStatusBadge } from "./SourceStatusBadge";
|
||||
|
||||
interface SourceCardProps {
|
||||
source: {
|
||||
id: string;
|
||||
name: string;
|
||||
data_type: string;
|
||||
adapter_id: string;
|
||||
status: string;
|
||||
item_count: number;
|
||||
last_synced: string | null;
|
||||
};
|
||||
source: SourceInfo;
|
||||
}
|
||||
|
||||
function formatRelative(iso: string): string {
|
||||
@@ -40,12 +33,16 @@ export function SourceCard({ source }: SourceCardProps) {
|
||||
className="border-app-line bg-app-box hover:border-app-line/80 hover:bg-app-hover group relative rounded-lg border p-4 text-left transition-all"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<SourceTypeIcon type={source.data_type} svg={getIcon(source.adapter_id)} size="md" />
|
||||
<SourceTypeIcon
|
||||
type={source.data_type}
|
||||
svg={source.adapter_id ? getIcon(source.adapter_id) : null}
|
||||
size="md"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-ink truncate text-sm font-medium">
|
||||
{source.name}
|
||||
</h3>
|
||||
<p className="text-ink-faint text-xs">{source.adapter_id}</p>
|
||||
<p className="text-ink-faint text-xs">{source.adapter_id ?? source.data_type}</p>
|
||||
</div>
|
||||
<SourceStatusBadge status={source.status} />
|
||||
</div>
|
||||
|
||||
@@ -350,12 +350,12 @@ function RuleFields({
|
||||
/>
|
||||
<Check
|
||||
label="regex"
|
||||
checked={rule.regex}
|
||||
checked={rule.regex ?? false}
|
||||
onChange={(regex) => onChange({ ...rule, regex })}
|
||||
/>
|
||||
<Check
|
||||
label="whole name"
|
||||
checked={rule.whole_name}
|
||||
checked={rule.whole_name ?? false}
|
||||
onChange={(whole_name) => onChange({ ...rule, whole_name })}
|
||||
/>
|
||||
</>
|
||||
@@ -409,8 +409,8 @@ function RuleFields({
|
||||
value={rule.pattern}
|
||||
onChange={(event) => onChange({ ...rule, pattern: event.target.value })}
|
||||
/>
|
||||
<Counter label="from" value={rule.start} onChange={(start) => onChange({ ...rule, start })} />
|
||||
<Counter label="step" value={rule.step} onChange={(step) => onChange({ ...rule, step })} />
|
||||
<Counter label="from" value={rule.start ?? 1} onChange={(start) => onChange({ ...rule, start })} />
|
||||
<Counter label="step" value={rule.step ?? 1} onChange={(step) => onChange({ ...rule, step })} />
|
||||
</>
|
||||
);
|
||||
case "template":
|
||||
|
||||
@@ -91,6 +91,10 @@ export const explorerRoutes = [
|
||||
{
|
||||
path: 'compare',
|
||||
element: <CompareVolumes />
|
||||
},
|
||||
{
|
||||
path: 'duplicates',
|
||||
element: <DuplicatesView />
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -98,7 +98,6 @@ export function SearchToolbar({ref}: {ref: Ref<HTMLDivElement>}) {
|
||||
className="flex flex-wrap items-center gap-2 px-3 py-1.5 border-b border-app-line/50"
|
||||
>
|
||||
<ToggleGroup
|
||||
size="sm"
|
||||
options={scopeOptions}
|
||||
value={scope === 'source' && !source ? 'folder' : scope}
|
||||
onChange={(value) =>
|
||||
|
||||
@@ -14,10 +14,6 @@ import {
|
||||
dialogManager,
|
||||
useDialog,
|
||||
CircleButton,
|
||||
TabsRoot,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "@spacedrive/primitives";
|
||||
import type {
|
||||
VolumeAddCloudInput,
|
||||
@@ -72,13 +68,6 @@ interface NetworkProtocol {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface JobOption {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface LocalFolderFormData {
|
||||
path: string;
|
||||
name: string;
|
||||
@@ -296,39 +285,6 @@ function StorageDialog({
|
||||
);
|
||||
}
|
||||
|
||||
const jobOptions: JobOption[] = [
|
||||
{
|
||||
id: "thumbnail",
|
||||
label: "Generate Thumbnails",
|
||||
description: "Create preview thumbnails for images and videos",
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: "thumbstrip",
|
||||
label: "Generate Thumbstrips",
|
||||
description: "Create video storyboard grids (5×5 grid of frames)",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: "proxy",
|
||||
label: "Generate Proxies",
|
||||
description: "Create scrubbing proxies for videos (~8s per video)",
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
id: "ocr",
|
||||
label: "Extract Text (OCR)",
|
||||
description: "OCR and text extraction from images/PDFs",
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
id: "speech_to_text",
|
||||
label: "Speech to Text",
|
||||
description: "Transcribe audio and video files",
|
||||
order: 5,
|
||||
},
|
||||
];
|
||||
|
||||
export function useAddStorageDialog(
|
||||
onStorageAdded?: (sdPath: any) => void,
|
||||
initialPath?: string,
|
||||
|
||||
@@ -4,36 +4,26 @@ import {
|
||||
Books,
|
||||
Browsers,
|
||||
Copy,
|
||||
Crop,
|
||||
Eye,
|
||||
FileArchive,
|
||||
FileText,
|
||||
FileVideo,
|
||||
FileZip,
|
||||
FilmStrip,
|
||||
FolderOpen,
|
||||
FolderPlus,
|
||||
FolderSimple,
|
||||
FolderSimplePlus,
|
||||
Image,
|
||||
LinkSimple,
|
||||
MagnifyingGlass,
|
||||
Microphone,
|
||||
Pencil,
|
||||
Scissors,
|
||||
ShareNetwork,
|
||||
SlidersHorizontal,
|
||||
Sparkle,
|
||||
Stack,
|
||||
Tag as TagIconComponent,
|
||||
TextAa,
|
||||
Trash,
|
||||
TreeStructure,
|
||||
Video,
|
||||
Waveform
|
||||
TreeStructure
|
||||
} from '@phosphor-icons/react';
|
||||
import type {File} from '@sd/ts-client';
|
||||
import {getContentKind, isVirtualFile} from '@sd/ts-client';
|
||||
import {isVirtualFile} from '@sd/ts-client';
|
||||
import { toast } from '@spacedrive/primitives';
|
||||
import {useFileOperationDialog} from '../../../components/modals/FileOperationModal';
|
||||
import {useMergeFoldersDialog} from '../../../components/modals/MergeFoldersModal';
|
||||
@@ -79,19 +69,6 @@ export function useFileContextMenu({
|
||||
const addToLibrary = useLibraryMutation('sources.track');
|
||||
const generateThumbnails = useLibraryMutation('thumbs.generate');
|
||||
|
||||
// Helper to run a mutation on each target file
|
||||
const forEachTarget = async (
|
||||
targets: File[],
|
||||
fn: (f: File) => Promise<unknown>
|
||||
) => {
|
||||
for (const f of targets) {
|
||||
try {
|
||||
await fn(f);
|
||||
} catch (err) {
|
||||
console.error(`Failed for ${f.name}:`, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
const clipboard = useClipboard();
|
||||
const openFileOperation = useFileOperationDialog();
|
||||
const openMergeFolders = useMergeFoldersDialog();
|
||||
|
||||
@@ -83,9 +83,10 @@ export function SourceDetail() {
|
||||
const deleteMutation = useLibraryMutation("sources.delete");
|
||||
const updateMutation = useLibraryMutation("adapters.update");
|
||||
|
||||
const adapterHasUpdate =
|
||||
adapters?.find((a) => a.id === source?.adapter_id)?.update_available ??
|
||||
false;
|
||||
// A filesystem source has no adapter, so there is nothing to update.
|
||||
const updatableAdapterId = adapters?.find(
|
||||
(a) => a.id === source?.adapter_id && a.update_available,
|
||||
)?.id;
|
||||
|
||||
// Sync tab title with source name
|
||||
const { activeTabId, updateTabTitle } = useTabManager();
|
||||
@@ -259,12 +260,11 @@ export function SourceDetail() {
|
||||
priority="normal"
|
||||
>
|
||||
<MoreActionsMenu
|
||||
adapterHasUpdate={adapterHasUpdate}
|
||||
onUpdate={() =>
|
||||
updateMutation.mutate({
|
||||
adapter_id: source.adapter_id,
|
||||
})
|
||||
}
|
||||
adapterHasUpdate={updatableAdapterId !== undefined}
|
||||
onUpdate={() => {
|
||||
if (updatableAdapterId === undefined) return;
|
||||
updateMutation.mutate({ adapter_id: updatableAdapterId });
|
||||
}}
|
||||
isUpdating={updateMutation.isPending}
|
||||
onDelete={() => setShowDeleteConfirm(true)}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
[toolchain]
|
||||
# gpui's floor: it uses std::hint::cold_path (stable 1.95+); zed pins 1.97.1.
|
||||
channel = "1.97.1"
|
||||
components = ["rustfmt", "clippy"]
|
||||
+26
-7
@@ -106,10 +106,12 @@ fi
|
||||
# Install system deps
|
||||
case "$(uname)" in
|
||||
"Darwin")
|
||||
# crates/ffmpeg is a default workspace member and links the system libav
|
||||
# libraries, so FFmpeg's headers must be present before the first build.
|
||||
if [ "$(uname -m)" = 'x86_64' ] && ! [ "${CI:-}" = "true" ]; then
|
||||
brew install nasm cmake
|
||||
brew install nasm cmake ffmpeg pkg-config
|
||||
elif ! [ "${CI:-}" = "true" ]; then
|
||||
brew install cmake
|
||||
brew install cmake ffmpeg pkg-config
|
||||
fi
|
||||
|
||||
# Install rust deps for iOS
|
||||
@@ -151,7 +153,12 @@ case "$(uname)" in
|
||||
set -- "$@" gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
|
||||
# C/C++ build dependencies, required to build some *-sys crates
|
||||
set -- "$@" llvm-dev libclang-dev clang nasm perl cmake
|
||||
set -- "$@" llvm-dev libclang-dev clang nasm perl cmake pkg-config
|
||||
|
||||
# FFmpeg headers: crates/ffmpeg (a default workspace member) links libav* and
|
||||
# ffmpeg-sys-next runs bindgen against them, which needs libclang
|
||||
set -- "$@" libavutil-dev libavformat-dev libavcodec-dev libavfilter-dev \
|
||||
libavdevice-dev libswscale-dev libswresample-dev
|
||||
|
||||
# React dependencies
|
||||
set -- "$@" libvips42
|
||||
@@ -169,7 +176,10 @@ case "$(uname)" in
|
||||
set -- "$@" gst-plugins-base gst-plugins-good gst-plugins-ugly
|
||||
|
||||
# C/C++ build dependencies, required to build some *-sys crates
|
||||
set -- "$@" clang nasm perl cmake
|
||||
set -- "$@" clang nasm perl cmake pkgconf
|
||||
|
||||
# FFmpeg headers for crates/ffmpeg
|
||||
set -- "$@" ffmpeg
|
||||
|
||||
# React dependencies
|
||||
set -- "$@" libvips
|
||||
@@ -194,7 +204,10 @@ case "$(uname)" in
|
||||
gstreamer1-plugins-good-extras gstreamer1-plugins-ugly-free
|
||||
|
||||
# C/C++ build dependencies, required to build some *-sys crates
|
||||
set -- "$@" clang clang-devel nasm perl-core cmake
|
||||
set -- "$@" clang clang-devel nasm perl-core cmake pkgconf-pkg-config
|
||||
|
||||
# FFmpeg headers for crates/ffmpeg (ffmpeg-free ships the libav*-devel packages)
|
||||
set -- "$@" ffmpeg-free-devel
|
||||
|
||||
# React dependencies
|
||||
set -- "$@" vips
|
||||
@@ -213,7 +226,10 @@ case "$(uname)" in
|
||||
set -- "$@" gst-plugins-base-dev gst-plugins-good gst-plugins-ugly
|
||||
|
||||
# C/C++ build dependencies, required to build some *-sys crates
|
||||
set -- "$@" llvm16-dev clang16 nasm perl cmake
|
||||
set -- "$@" llvm16-dev clang16 nasm perl cmake pkgconf
|
||||
|
||||
# FFmpeg headers for crates/ffmpeg
|
||||
set -- "$@" ffmpeg-dev
|
||||
|
||||
# React dependencies
|
||||
set -- "$@" vips
|
||||
@@ -232,7 +248,10 @@ case "$(uname)" in
|
||||
set -- "$@" gstreamer-1.0-plugins-good gstreamer-1.0-plugins-ugly gstreamer-1.0-devel gstreamer-1.0-plugins-base-devel
|
||||
|
||||
# C/C++ build dependencies, required to build some *-sys crates
|
||||
set -- "$@" llvm-devel llvm-clang-devel llvm-clang nasm perl cmake
|
||||
set -- "$@" llvm-devel llvm-clang-devel llvm-clang nasm perl cmake pkg-config
|
||||
|
||||
# FFmpeg headers for crates/ffmpeg
|
||||
set -- "$@" ffmpeg-devel
|
||||
|
||||
# React dependencies
|
||||
set -- "$@" libvips
|
||||
|
||||
@@ -151,6 +151,27 @@ fn setup(with_native_deps: bool) -> Result<()> {
|
||||
}
|
||||
println!(" ✓ Rust toolchain found");
|
||||
|
||||
// crates/ffmpeg is a default workspace member that links the system libav
|
||||
// libraries, so a missing libavutil fails the first `cargo build` twelve
|
||||
// minutes in. Warn up front and point at the script that installs it.
|
||||
let has_libav = Command::new("pkg-config")
|
||||
.args([
|
||||
"--exists",
|
||||
"libavutil",
|
||||
"libavformat",
|
||||
"libavcodec",
|
||||
"libavfilter",
|
||||
])
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(false);
|
||||
if has_libav {
|
||||
println!(" ✓ FFmpeg development libraries found");
|
||||
} else {
|
||||
println!(" ! FFmpeg development libraries (libavutil, libavformat, libavcodec, libavfilter) not found via pkg-config");
|
||||
println!(" Run ./scripts/setup.sh to install them, or set PKG_CONFIG_PATH if FFmpeg lives in a custom prefix");
|
||||
}
|
||||
|
||||
// The bundle carries FFmpeg, libheif and Pdfium, which only the `ffmpeg` and
|
||||
// `heif` features link against. Neither is on by default, and macOS reaches
|
||||
// the same formats through ImageIO and QuickLook, so the download is opt-in.
|
||||
|
||||
+5
-7
@@ -171,13 +171,11 @@ pub fn has_linker(name: &str) -> bool {
|
||||
|
||||
/// Get the best available linker for the platform
|
||||
pub fn get_best_linker() -> Option<String> {
|
||||
if cfg!(target_os = "linux") {
|
||||
if has_linker("clang") {
|
||||
if has_linker("mold") {
|
||||
return Some("mold".to_string());
|
||||
} else if has_linker("lld") {
|
||||
return Some("lld".to_string());
|
||||
}
|
||||
if cfg!(target_os = "linux") && has_linker("clang") {
|
||||
if has_linker("mold") {
|
||||
return Some("mold".to_string());
|
||||
} else if has_linker("lld") {
|
||||
return Some("lld".to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
|
||||
Reference in new issue
Block a user