wasm_pkg_client/
local.rs

1//! Local filesystem-based package backend.
2//!
3//! Each package release is a file: `<root>/<namespace>/<name>/<version>.wasm`
4
5use std::path::PathBuf;
6
7use anyhow::anyhow;
8use async_trait::async_trait;
9use futures_util::{StreamExt, TryStreamExt};
10use serde::Deserialize;
11use tokio_util::io::ReaderStream;
12use wasm_pkg_common::{
13    config::RegistryConfig,
14    digest::ContentDigest,
15    package::{PackageRef, Version},
16    Error,
17};
18
19use crate::{
20    loader::PackageLoader,
21    publisher::PackagePublisher,
22    release::{Release, VersionInfo},
23    ContentStream, PublishingSource,
24};
25
26#[derive(Clone, Debug, Deserialize)]
27pub struct LocalConfig {
28    pub root: PathBuf,
29}
30
31pub(crate) struct LocalBackend {
32    root: PathBuf,
33}
34
35impl LocalBackend {
36    pub fn new(registry_config: RegistryConfig) -> Result<Self, Error> {
37        let config = registry_config
38            .backend_config::<LocalConfig>("local")?
39            .ok_or_else(|| {
40                Error::InvalidConfig(anyhow!("'local' backend requires configuration"))
41            })?;
42        Ok(Self { root: config.root })
43    }
44
45    fn package_dir(&self, package: &PackageRef) -> PathBuf {
46        self.root
47            .join(package.namespace().as_ref())
48            .join(package.name().as_ref())
49    }
50
51    fn version_path(&self, package: &PackageRef, version: &Version) -> PathBuf {
52        self.package_dir(package).join(format!("{version}.wasm"))
53    }
54}
55
56#[async_trait]
57impl PackageLoader for LocalBackend {
58    async fn list_all_versions(&self, package: &PackageRef) -> Result<Vec<VersionInfo>, Error> {
59        let mut versions = vec![];
60        let package_dir = self.package_dir(package);
61        tracing::debug!(?package_dir, "Reading versions from path");
62        let mut entries = tokio::fs::read_dir(package_dir).await?;
63        while let Some(entry) = entries.next_entry().await? {
64            let path = entry.path();
65            if path.extension() != Some("wasm".as_ref()) {
66                continue;
67            }
68            let Some(version) = path
69                .file_stem()
70                .unwrap()
71                .to_str()
72                .and_then(|stem| Version::parse(stem).ok())
73            else {
74                tracing::warn!("invalid package file name at {path:?}");
75                continue;
76            };
77            versions.push(VersionInfo {
78                version,
79                yanked: false,
80            });
81        }
82        Ok(versions)
83    }
84
85    async fn get_release(&self, package: &PackageRef, version: &Version) -> Result<Release, Error> {
86        let path = self.version_path(package, version);
87        tracing::debug!(path = %path.display(), "Reading content from path");
88        let content_digest = ContentDigest::sha256_from_file(path).await?;
89        Ok(Release {
90            version: version.clone(),
91            content_digest,
92        })
93    }
94
95    async fn stream_content_unvalidated(
96        &self,
97        package: &PackageRef,
98        content: &Release,
99    ) -> Result<ContentStream, Error> {
100        let path = self.version_path(package, &content.version);
101        tracing::debug!("Streaming content from {path:?}");
102        let file = tokio::fs::File::open(path).await?;
103        Ok(ReaderStream::new(file).map_err(Into::into).boxed())
104    }
105}
106
107#[async_trait::async_trait]
108impl PackagePublisher for LocalBackend {
109    async fn publish(
110        &self,
111        package: &PackageRef,
112        version: &Version,
113        mut data: PublishingSource,
114    ) -> Result<(), Error> {
115        let package_dir = self.package_dir(package);
116        // Ensure the package directory exists.
117        tokio::fs::create_dir_all(package_dir).await?;
118        let path = self.version_path(package, version);
119        let mut out = tokio::fs::File::create(path).await?;
120        tokio::io::copy(&mut data, &mut out)
121            .await
122            .map_err(Error::IoError)
123            .map(|_| ())
124    }
125}