Skip to content

Commit 14a8aa5

Browse files
authored
feat: Use checksum to dedupe uploaded release artifacts (#1275)
* feat: Use checksum to dedupe uploaded release artifacts
1 parent 5987271 commit 14a8aa5

10 files changed

+291
-2
lines changed

src/commands/files/upload.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
151151
ty: SourceFileType::Source,
152152
headers: headers.clone(),
153153
messages: vec![],
154+
already_uploaded: false,
154155
},
155156
)
156157
})

src/commands/sourcemaps/upload.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::path::PathBuf;
2+
use std::str::FromStr;
23

3-
use anyhow::Result;
4+
use anyhow::{format_err, Result};
45
use clap::{Arg, ArgMatches, Command};
56
use glob::{glob_with, MatchOptions};
67
use log::{debug, warn};
8+
use sha1_smol::Digest;
79

810
use crate::api::{Api, NewRelease};
911
use crate::config::Config;
@@ -303,6 +305,13 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
303305
let api = Api::current();
304306
let mut processor = SourceMapProcessor::new();
305307

308+
for artifact in api.list_release_files(&org, Some(&project), &version)? {
309+
let checksum = Digest::from_str(&artifact.sha1)
310+
.map_err(|_| format_err!("Invalid artifact checksum"))?;
311+
312+
processor.add_already_uploaded_source(checksum);
313+
}
314+
306315
if matches.is_present("bundle") && matches.is_present("bundle_sourcemap") {
307316
process_sources_from_bundle(matches, &mut processor)?;
308317
} else {

src/utils/file_upload.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ use console::style;
1212
use parking_lot::RwLock;
1313
use rayon::prelude::*;
1414
use rayon::ThreadPoolBuilder;
15+
use sha1_smol::Digest;
1516
use symbolic::common::ByteView;
1617
use symbolic::debuginfo::sourcebundle::{SourceBundleWriter, SourceFileInfo, SourceFileType};
1718
use url::Url;
1819

1920
use crate::api::{Api, ChunkUploadCapability, ChunkUploadOptions, FileContents, ProgressBarMode};
2021
use crate::constants::DEFAULT_MAX_WAIT;
2122
use crate::utils::chunks::{upload_chunks, Chunk, ASSEMBLE_POLL_INTERVAL};
22-
use crate::utils::fs::{get_sha1_checksums, TempFile};
23+
use crate::utils::fs::{get_sha1_checksum, get_sha1_checksums, TempFile};
2324
use crate::utils::progress::{ProgressBar, ProgressStyle};
2425

2526
/// Fallback concurrency for release file uploads.
@@ -56,9 +57,15 @@ pub struct ReleaseFile {
5657
pub ty: SourceFileType,
5758
pub headers: Vec<(String, String)>,
5859
pub messages: Vec<(LogLevel, String)>,
60+
pub already_uploaded: bool,
5961
}
6062

6163
impl ReleaseFile {
64+
/// Calculates and returns the SHA1 checksum of the file.
65+
pub fn checksum(&self) -> Result<Digest> {
66+
get_sha1_checksum(&*self.contents)
67+
}
68+
6269
pub fn log(&mut self, level: LogLevel, msg: String) {
6370
self.messages.push((level, msg));
6471
}

src/utils/sourcemaps.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use anyhow::{bail, Error, Result};
99
use console::style;
1010
use indicatif::ProgressStyle;
1111
use log::{debug, info, warn};
12+
use sha1_smol::Digest;
1213
use symbolic::debuginfo::sourcebundle::SourceFileType;
1314
use url::Url;
1415

@@ -147,6 +148,7 @@ fn guess_sourcemap_reference(sourcemaps: &HashSet<String>, min_url: &str) -> Res
147148

148149
pub struct SourceMapProcessor {
149150
pending_sources: HashSet<(String, ReleaseFileMatch)>,
151+
already_uploaded_sources: Vec<Digest>,
150152
sources: ReleaseFiles,
151153
}
152154

@@ -162,6 +164,7 @@ impl SourceMapProcessor {
162164
pub fn new() -> SourceMapProcessor {
163165
SourceMapProcessor {
164166
pending_sources: HashSet::new(),
167+
already_uploaded_sources: Vec::new(),
165168
sources: HashMap::new(),
166169
}
167170
}
@@ -172,6 +175,11 @@ impl SourceMapProcessor {
172175
Ok(())
173176
}
174177

178+
/// Adds an already uploaded sources checksum.
179+
pub fn add_already_uploaded_source(&mut self, checksum: Digest) {
180+
self.already_uploaded_sources.push(checksum);
181+
}
182+
175183
fn flush_pending_sources(&mut self) {
176184
if self.pending_sources.is_empty() {
177185
return;
@@ -191,6 +199,7 @@ impl SourceMapProcessor {
191199
);
192200
for (url, mut file) in self.pending_sources.drain() {
193201
pb.set_message(&url);
202+
194203
let ty = if sourcemap::is_sourcemap_slice(&file.contents) {
195204
SourceFileType::SourceMap
196205
} else if file
@@ -232,6 +241,7 @@ impl SourceMapProcessor {
232241
ty,
233242
headers: vec![],
234243
messages: vec![],
244+
already_uploaded: false,
235245
},
236246
);
237247
pb.inc(1);
@@ -268,6 +278,14 @@ impl SourceMapProcessor {
268278
sect = Some(source.ty);
269279
}
270280

281+
if source.already_uploaded {
282+
println!(
283+
" {}",
284+
style(format!("{} (skipped; already uploaded)", &source.url)).yellow()
285+
);
286+
continue;
287+
}
288+
271289
if source.ty == SourceFileType::MinifiedSource {
272290
if let Some(sm_ref) = get_sourcemap_ref(source) {
273291
let url = sm_ref.get_url();
@@ -401,6 +419,7 @@ impl SourceMapProcessor {
401419
ty: SourceFileType::MinifiedSource,
402420
headers: vec![],
403421
messages: vec![],
422+
already_uploaded: false,
404423
},
405424
);
406425

@@ -418,6 +437,7 @@ impl SourceMapProcessor {
418437
ty: SourceFileType::SourceMap,
419438
headers: vec![],
420439
messages: vec![],
440+
already_uploaded: false,
421441
},
422442
);
423443
}
@@ -527,9 +547,20 @@ impl SourceMapProcessor {
527547
Ok(())
528548
}
529549

550+
fn flag_uploaded_sources(&mut self) {
551+
for source in self.sources.values_mut() {
552+
if let Ok(checksum) = &source.checksum() {
553+
if self.already_uploaded_sources.contains(checksum) {
554+
source.already_uploaded = true;
555+
}
556+
}
557+
}
558+
}
559+
530560
/// Uploads all files
531561
pub fn upload(&mut self, context: &UploadContext<'_>) -> Result<()> {
532562
self.flush_pending_sources();
563+
self.flag_uploaded_sources();
533564
let mut uploader = ReleaseFileUpload::new(context);
534565
uploader.files(&self.sources);
535566
uploader.upload()?;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
```
2+
$ sentry-cli sourcemaps upload --help
3+
? success
4+
sentry-cli[EXE]-sourcemaps-upload
5+
Upload sourcemaps for a release.
6+
7+
USAGE:
8+
sentry-cli[EXE] sourcemaps upload [OPTIONS] [PATHS]...
9+
10+
ARGS:
11+
<PATHS>... The files to upload.
12+
13+
OPTIONS:
14+
--auth-token <AUTH_TOKEN>
15+
Use the given Sentry auth token.
16+
17+
--bundle <BUNDLE>
18+
Path to the application bundle (indexed, file, or regular)
19+
20+
--bundle-sourcemap <BUNDLE_SOURCEMAP>
21+
Path to the bundle sourcemap
22+
23+
-d, --dist <DISTRIBUTION>
24+
Optional distribution identifier for the sourcemaps.
25+
26+
-h, --help
27+
Print help information
28+
29+
--header <KEY:VALUE>
30+
Custom headers that should be attached to all requests
31+
in key:value format.
32+
33+
-i, --ignore <IGNORE>
34+
Ignores all files and folders matching the given glob
35+
36+
-I, --ignore-file <IGNORE_FILE>
37+
Ignore all files and folders specified in the given ignore file, e.g. .gitignore.
38+
39+
--log-level <LOG_LEVEL>
40+
Set the log output verbosity. [possible values: trace, debug, info, warn, error]
41+
42+
--no-rewrite
43+
Disables rewriting of matching sourcemaps. By default the tool will rewrite sources, so
44+
that indexed maps are flattened and missing sources are inlined if possible.
45+
This fundamentally changes the upload process to be based on sourcemaps and minified
46+
files exclusively and comes in handy for setups like react-native that generate
47+
sourcemaps that would otherwise not work for sentry.
48+
49+
--no-sourcemap-reference
50+
Disable emitting of automatic sourcemap references.
51+
By default the tool will store a 'Sourcemap' header with minified files so that
52+
sourcemaps are located automatically if the tool can detect a link. If this causes
53+
issues it can be disabled.
54+
55+
-o, --org <ORG>
56+
The organization slug
57+
58+
-p, --project <PROJECT>
59+
The project slug.
60+
61+
--quiet
62+
Do not print any output while preserving correct exit code. This flag is currently
63+
implemented only for selected subcommands. [aliases: silent]
64+
65+
-r, --release <RELEASE>
66+
The release slug.
67+
68+
--strip-common-prefix
69+
Similar to --strip-prefix but strips the most common prefix on all sources references.
70+
71+
--strip-prefix <PREFIX>
72+
Strips the given prefix from all sources references inside the upload sourcemaps (paths
73+
used within the sourcemap content, to map minified code to it's original source). Only
74+
sources that start with the given prefix will be stripped.
75+
This will not modify the uploaded sources paths. To do that, point the upload or
76+
upload-sourcemaps command to a more precise directory instead.
77+
78+
-u, --url-prefix <PREFIX>
79+
The URL prefix to prepend to all filenames.
80+
81+
--url-suffix <SUFFIX>
82+
The URL suffix to append to all filenames.
83+
84+
--validate
85+
Enable basic sourcemap validation.
86+
87+
--wait
88+
Wait for the server to fully process uploaded files.
89+
90+
-x, --ext <EXT>
91+
Set the file extensions that are considered for upload. This overrides the default
92+
extensions. To add an extension, all default extensions must be repeated. Specify once
93+
per extension.
94+
Defaults to: `--ext=js --ext=map --ext=jsbundle --ext=bundle`
95+
96+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
```
2+
$ sentry-cli sourcemaps upload tests/integration/_fixtures/bundle.min.js.map --release=wat-release
3+
? success
4+
> Found 1 release file
5+
> Analyzing 1 sources
6+
> Rewriting sources
7+
> Adding source map references
8+
> Bundled 1 file for upload
9+
> Uploaded release files to Sentry
10+
> File upload complete (processing pending on server)
11+
> Organization: wat-org
12+
> Project: wat-project
13+
> Release: wat-release
14+
> Dist: None
15+
16+
Source Map Upload Report
17+
Source Maps
18+
~/bundle.min.js.map (skipped; already uploaded)
19+
20+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
```
2+
$ sentry-cli sourcemaps upload tests/integration/_fixtures/bundle.min.js.map --release=wat-release
3+
? success
4+
> Found 1 release file
5+
> Analyzing 1 sources
6+
> Rewriting sources
7+
> Adding source map references
8+
> Bundled 1 file for upload
9+
> Uploaded release files to Sentry
10+
> File upload complete (processing pending on server)
11+
> Organization: wat-org
12+
> Project: wat-project
13+
> Release: wat-release
14+
> Dist: None
15+
16+
Source Map Upload Report
17+
Source Maps
18+
~/bundle.min.js.map
19+
20+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
```
2+
$ sentry-cli sourcemaps upload
3+
? failed
4+
error: The following required arguments were not provided:
5+
<PATHS>...
6+
7+
USAGE:
8+
sentry-cli[EXE] sourcemaps upload <PATHS>...
9+
10+
For more information try --help
11+
12+
```

tests/integration/sourcemaps/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::integration::register_test;
22

33
mod explain;
44
mod resolve;
5+
mod upload;
56

67
#[test]
78
fn command_sourcemaps_help() {

0 commit comments

Comments
 (0)