1use 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 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}