Skip to content

Commit 813fa81

Browse files
ref: Separate LegacyUploadContext for legacy uploads
Legacy (i.e. non-chunked) are distinct from chunked uploads, so it makes sense to have a separate struct to enforce these differences via the type system. Specifically, legacy uploads must be associated with a release, whereas this is not required for many chunk uploads. Legacy uploads must also be associated with at most one project, whereas chunked uploads can have multiple projects (support for which we will add in the CLI with #2408)
1 parent b241c4a commit 813fa81

File tree

3 files changed

+130
-19
lines changed

3 files changed

+130
-19
lines changed

src/api/mod.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ use uuid::Uuid;
4747
use crate::api::errors::ProjectRenamedError;
4848
use crate::config::{Auth, Config};
4949
use crate::constants::{ARCH, DEFAULT_URL, EXT, PLATFORM, RELEASE_REGISTRY_LATEST_URL, VERSION};
50-
use crate::utils::file_upload::UploadContext;
50+
use crate::utils::file_upload::LegacyUploadContext;
5151
use crate::utils::http::{self, is_absolute_url};
5252
use crate::utils::progress::{ProgressBar, ProgressBarMode};
5353
use crate::utils::retry::{get_default_backoff, DurationAsMilliseconds};
@@ -1425,28 +1425,24 @@ impl RegionSpecificApi<'_> {
14251425
/// system and uploaded as `name`.
14261426
pub fn upload_release_file(
14271427
&self,
1428-
context: &UploadContext,
1428+
context: &LegacyUploadContext,
14291429
contents: &[u8],
14301430
name: &str,
14311431
headers: Option<&[(String, String)]>,
14321432
progress_bar_mode: ProgressBarMode,
14331433
) -> ApiResult<Option<Artifact>> {
1434-
let release = context
1435-
.release()
1436-
.map_err(|err| ApiError::with_source(ApiErrorKind::ReleaseNotFound, err))?;
1437-
1438-
let path = if let Some(project) = context.project {
1434+
let path = if let Some(project) = context.project() {
14391435
format!(
14401436
"/projects/{}/{}/releases/{}/files/",
1441-
PathArg(context.org),
1437+
PathArg(context.org()),
14421438
PathArg(project),
1443-
PathArg(release)
1439+
PathArg(context.release())
14441440
)
14451441
} else {
14461442
format!(
14471443
"/organizations/{}/releases/{}/files/",
1448-
PathArg(context.org),
1449-
PathArg(release)
1444+
PathArg(context.org()),
1445+
PathArg(context.release())
14501446
)
14511447
};
14521448
let mut form = curl::easy::Form::new();
@@ -1459,7 +1455,7 @@ impl RegionSpecificApi<'_> {
14591455
.buffer(filename, contents.to_vec())
14601456
.add()?;
14611457
form.part("name").contents(name.as_bytes()).add()?;
1462-
if let Some(dist) = context.dist {
1458+
if let Some(dist) = context.dist() {
14631459
form.part("dist").contents(dist.as_bytes()).add()?;
14641460
}
14651461

src/commands/files/upload.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
244244
if let Some(artifact) = authenticated_api
245245
.region_specific(context.org)
246246
.upload_release_file(
247-
context,
247+
&context.try_into()?,
248248
&contents,
249249
name,
250250
Some(

src/utils/file_upload.rs

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Searches, processes and uploads release files.
22
use std::collections::{BTreeMap, HashMap};
3-
use std::fmt;
3+
use std::fmt::{self, Display};
44
use std::io::BufWriter;
55
use std::path::PathBuf;
66
use std::str;
@@ -19,6 +19,7 @@ use symbolic::common::ByteView;
1919
use symbolic::debuginfo::sourcebundle::{
2020
SourceBundleErrorKind, SourceBundleWriter, SourceFileInfo, SourceFileType,
2121
};
22+
use thiserror::Error;
2223
use url::Url;
2324

2425
use crate::api::NewRelease;
@@ -100,6 +101,108 @@ impl UploadContext<'_> {
100101
}
101102
}
102103

104+
#[derive(Debug, Error)]
105+
pub enum LegacyUploadContextError {
106+
#[error("a release is required for this upload")]
107+
ReleaseMissing,
108+
}
109+
110+
/// Represents the context for legacy release uploads.
111+
///
112+
/// `LegacyUploadContext` contains information needed for legacy (non-chunked)
113+
/// uploads. Legacy uploads are primarily used when uploading to old self-hosted
114+
/// Sentry servers, which do not support receiving chunked uploads.
115+
///
116+
/// Unlike chunked uploads, legacy uploads require a release to be set,
117+
/// and do not need to have chunk-upload-related fields.
118+
#[derive(Debug, Default)]
119+
pub struct LegacyUploadContext<'a> {
120+
org: &'a str,
121+
project: Option<&'a str>,
122+
release: &'a str,
123+
dist: Option<&'a str>,
124+
}
125+
126+
impl LegacyUploadContext<'_> {
127+
pub fn org(&self) -> &str {
128+
self.org
129+
}
130+
131+
pub fn project(&self) -> Option<&str> {
132+
self.project
133+
}
134+
135+
pub fn release(&self) -> &str {
136+
self.release
137+
}
138+
139+
pub fn dist(&self) -> Option<&str> {
140+
self.dist
141+
}
142+
}
143+
144+
impl Display for LegacyUploadContext<'_> {
145+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146+
writeln!(
147+
f,
148+
"{} {}",
149+
style("> Organization:").dim(),
150+
style(self.org).yellow()
151+
)?;
152+
writeln!(
153+
f,
154+
"{} {}",
155+
style("> Project:").dim(),
156+
style(self.project.unwrap_or("None")).yellow()
157+
)?;
158+
writeln!(
159+
f,
160+
"{} {}",
161+
style("> Release:").dim(),
162+
style(self.release).yellow()
163+
)?;
164+
writeln!(
165+
f,
166+
"{} {}",
167+
style("> Dist:").dim(),
168+
style(self.dist.unwrap_or("None")).yellow()
169+
)?;
170+
write!(
171+
f,
172+
"{} {}",
173+
style("> Upload type:").dim(),
174+
style("single file/legacy upload").yellow()
175+
)
176+
}
177+
}
178+
179+
impl<'a> TryFrom<&'a UploadContext<'_>> for LegacyUploadContext<'a> {
180+
type Error = LegacyUploadContextError;
181+
182+
fn try_from(value: &'a UploadContext) -> Result<Self, Self::Error> {
183+
let &UploadContext {
184+
org,
185+
project,
186+
release,
187+
dist,
188+
note: _,
189+
wait: _,
190+
max_wait: _,
191+
dedupe: _,
192+
chunk_upload_options: _,
193+
} = value;
194+
195+
let release = release.ok_or(LegacyUploadContextError::ReleaseMissing)?;
196+
197+
Ok(Self {
198+
org,
199+
project,
200+
release,
201+
dist,
202+
})
203+
}
204+
}
205+
103206
#[derive(Eq, PartialEq, Debug, Copy, Clone)]
104207
pub enum LogLevel {
105208
Warning,
@@ -215,7 +318,19 @@ impl<'a> FileUpload<'a> {
215318
.context
216319
.chunk_upload_options
217320
.map_or(DEFAULT_CONCURRENCY, |o| usize::from(o.concurrency));
218-
upload_files_parallel(self.context, &self.files, concurrency)
321+
322+
let legacy_context = &self.context.try_into().map_err(|e| {
323+
anyhow::anyhow!(
324+
"Error while performing legacy upload: {e}. \
325+
If you would like to upload files {}, you need to upgrade your Sentry server \
326+
or switch to our SaaS offering.",
327+
match e {
328+
LegacyUploadContextError::ReleaseMissing => "without specifying a release",
329+
}
330+
)
331+
})?;
332+
333+
upload_files_parallel(legacy_context, &self.files, concurrency)
219334
}
220335

221336
pub fn build_jvm_bundle(&self, debug_id: Option<DebugId>) -> Result<TempFile> {
@@ -224,18 +339,18 @@ impl<'a> FileUpload<'a> {
224339
}
225340

226341
fn upload_files_parallel(
227-
context: &UploadContext,
342+
context: &LegacyUploadContext,
228343
files: &SourceFiles,
229344
num_threads: usize,
230345
) -> Result<()> {
231346
let api = Api::current();
232-
let release = context.release()?;
347+
let release = context.release();
233348

234349
// get a list of release files first so we know the file IDs of
235350
// files that already exist.
236351
let release_files: HashMap<_, _> = api
237352
.authenticated()?
238-
.list_release_files(context.org, context.project, release)?
353+
.list_release_files(context.org, context.project(), release)?
239354
.into_iter()
240355
.map(|artifact| ((artifact.dist, artifact.name), artifact.id))
241356
.collect();
@@ -308,7 +423,7 @@ fn upload_files_parallel(
308423

309424
pb.finish_and_clear();
310425

311-
print_upload_context_details(context);
426+
println!("{}", context);
312427

313428
Ok(())
314429
}

0 commit comments

Comments
 (0)