Skip to content

Commit 8a63fec

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 8a63fec

File tree

3 files changed

+126
-19
lines changed

3 files changed

+126
-19
lines changed

src/api/mod.rs

+8-12
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

+1-1
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

+117-6
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,104 @@ 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+
..
189+
} = value;
190+
191+
let release = release.ok_or(LegacyUploadContextError::ReleaseMissing)?;
192+
193+
Ok(Self {
194+
org,
195+
project,
196+
release,
197+
dist,
198+
})
199+
}
200+
}
201+
103202
#[derive(Eq, PartialEq, Debug, Copy, Clone)]
104203
pub enum LogLevel {
105204
Warning,
@@ -215,7 +314,19 @@ impl<'a> FileUpload<'a> {
215314
.context
216315
.chunk_upload_options
217316
.map_or(DEFAULT_CONCURRENCY, |o| usize::from(o.concurrency));
218-
upload_files_parallel(self.context, &self.files, concurrency)
317+
318+
let legacy_context = &self.context.try_into().map_err(|e| {
319+
anyhow::anyhow!(
320+
"Error while performing legacy upload: {e}. \
321+
If you would like to upload files {}, you need to upgrade your Sentry server \
322+
or switch to our SaaS offering.",
323+
match e {
324+
LegacyUploadContextError::ReleaseMissing => "without specifying a release",
325+
}
326+
)
327+
})?;
328+
329+
upload_files_parallel(legacy_context, &self.files, concurrency)
219330
}
220331

221332
pub fn build_jvm_bundle(&self, debug_id: Option<DebugId>) -> Result<TempFile> {
@@ -224,18 +335,18 @@ impl<'a> FileUpload<'a> {
224335
}
225336

226337
fn upload_files_parallel(
227-
context: &UploadContext,
338+
context: &LegacyUploadContext,
228339
files: &SourceFiles,
229340
num_threads: usize,
230341
) -> Result<()> {
231342
let api = Api::current();
232-
let release = context.release()?;
343+
let release = context.release();
233344

234345
// get a list of release files first so we know the file IDs of
235346
// files that already exist.
236347
let release_files: HashMap<_, _> = api
237348
.authenticated()?
238-
.list_release_files(context.org, context.project, release)?
349+
.list_release_files(context.org, context.project(), release)?
239350
.into_iter()
240351
.map(|artifact| ((artifact.dist, artifact.name), artifact.id))
241352
.collect();
@@ -308,7 +419,7 @@ fn upload_files_parallel(
308419

309420
pb.finish_and_clear();
310421

311-
print_upload_context_details(context);
422+
println!("{}", context);
312423

313424
Ok(())
314425
}

0 commit comments

Comments
 (0)