Compare commits

..

121 commits

Author SHA1 Message Date
FlashOnFire_ 5286618648 fix: limit folder names length to avoid crashes
Some checks failed
Clippy / clippy (push) Has been cancelled
code-coverage / Rust project (push) Has been cancelled
Build and test / lint (push) Has been cancelled
Build and test / Build ${{ matrix.target }}-${{ matrix.rust }} on ${{ matrix.os }} (ubuntu-latest, 1.70.0, riscv64gc-unknown-linux-gnu) (push) Has been cancelled
Build and test / Build ${{ matrix.target }}-${{ matrix.rust }} on ${{ matrix.os }} (ubuntu-latest, 1.70.0, x86_64-unknown-linux-gnu) (push) Has been cancelled
Build and test / Build ${{ matrix.target }}-${{ matrix.rust }} on ${{ matrix.os }} (ubuntu-latest, stable, aarch64-unknown-linux-gnu) (push) Has been cancelled
Build and test / Build ${{ matrix.target }}-${{ matrix.rust }} on ${{ matrix.os }} (ubuntu-latest, stable, riscv64gc-unknown-linux-gnu) (push) Has been cancelled
Build and test / Build ${{ matrix.target }}-${{ matrix.rust }} on ${{ matrix.os }} (ubuntu-latest, stable, x86_64-unknown-linux-gnu) (push) Has been cancelled
Build and test / Tests ${{ matrix.target }} on ${{ matrix.os }} (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Has been cancelled
Build and test / Build ${{ matrix.target }}-${{ matrix.rust }} on ${{ matrix.os }} (ubuntu-latest, 1.70.0, aarch64-unknown-linux-gnu) (push) Has been cancelled
2026-03-12 11:18:03 +01:00
FlashOnFire_ 301aa53830 fix: avoid panic when encountering malformed URLs 2026-03-12 11:18:03 +01:00
dependabot[bot] f513cc541d build(deps): bump mio from 0.8.10 to 0.8.11
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-05 09:38:09 +01:00
Esteban Blanc 4ca5f0c104 misc: Update Cargo.lock
Signed-off-by: Esteban Blanc <estblcsk@gmail.com>
2024-01-24 16:48:59 +01:00
Esteban Blanc e47b71fe36 misc: Update Nix flake
Signed-off-by: Esteban Blanc <estblcsk@gmail.com>
2024-01-24 16:48:59 +01:00
dependabot[bot] 9f492a8b8d robo(ci): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 11:09:04 +01:00
dependabot[bot] 7462ab1ff7 build(deps): bump h2 from 0.3.21 to 0.3.24
Bumps [h2](https://github.com/hyperium/h2) from 0.3.21 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.21...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 10:57:03 +01:00
dependabot[bot] 0aa160e036 robo(ci): bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-16 10:24:44 +01:00
dependabot[bot] 4bcf2705a9
build(deps): bump rustix from 0.38.14 to 0.38.21 (#232) 2023-12-06 10:22:12 +00:00
dependabot[bot] 39b751d245 build(deps): bump openssl from 0.10.57 to 0.10.60
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.57 to 0.10.60.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.57...openssl-v0.10.60)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-29 11:22:15 +01:00
marchellodev 0cae95f893 readme: add --cookie option 2023-10-12 12:06:30 +02:00
marchellodev be91ca0261 downloader: add cookie support 2023-10-12 12:06:30 +02:00
Esteban Blanc 2c1f6a86d9 misc: Update MSRV
Signed-off-by: Esteban Blanc <estblcsk@gmail.com>
2023-10-11 10:29:22 +02:00
Esteban Blanc c488442982 ci: Fix libssl install for cross-compile
Signed-off-by: Esteban Blanc <estblcsk@gmail.com>
2023-10-11 10:29:22 +02:00
dependabot[bot] 103d4a7505 build(deps): bump openssl from 0.10.50 to 0.10.55
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.50 to 0.10.55.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.50...openssl-v0.10.55)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-22 10:42:15 +02:00
Esteban Blanc 7f6c4c34a5 tests: Auto cleanup of download folders + multiple servers
Signed-off-by: Esteban Blanc <estblcsk@gmail.com>
2023-05-02 10:18:59 +02:00
Esteban Blanc 786503677a ci: Fix coverage
Signed-off-by: Esteban Blanc <estblcsk@gmail.com>
2023-05-02 10:18:59 +02:00
Esteban Blanc c080df3c96 tests: Fix server returned content
Signed-off-by: Esteban Blanc <estblcsk@gmail.com>
2023-05-02 10:18:59 +02:00
Esteban Blanc 208c747165 misc: Update dependencies and bump MSRV
Signed-off-by: Esteban Blanc <estblcsk@gmail.com>
2023-04-13 23:57:07 +02:00
Esteban Blanc 3d5d8d148a ci: Update tarpaulin
Signed-off-by: Esteban Blanc <estblcsk@gmail.com>
2023-04-13 23:57:07 +02:00
dependabot[bot] 05a76ba263 build(deps): bump openssl from 0.10.45 to 0.10.48
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.45 to 0.10.48.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.45...openssl-v0.10.48)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-26 14:24:56 +02:00
dependabot[bot] bcfbacf388 build(deps): bump tokio from 1.24.1 to 1.25.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.24.1 to 1.25.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.24.1...tokio-1.25.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-12 14:14:30 +01:00
Esteban Blanc 906db51f4e mics: update MSRV 2023-01-18 16:35:49 +01:00
Esteban Blanc 28c1e5f3a4 misc: update dependencies 2023-01-18 16:35:49 +01:00
Alejandro Osornio d7aa100c51 Rm match redundancy 2023-01-15 23:14:10 +01:00
Alejandro Osornio 6e9bd7dc4e Re-include fragments 2023-01-15 23:14:10 +01:00
Esteban Blanc 379bfef501 build: add flake.nix for nix users 2023-01-14 16:38:02 +01:00
dependabot[bot] 0c8164c145 build(deps): bump tokio from 1.21.2 to 1.24.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.21.2 to 1.24.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.21.2...tokio-1.24.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-07 20:06:13 +01:00
dependabot[bot] 4c2a255e9f robo(ci): bump bruceadams/get-release from 1.2.3 to 1.3.2
Bumps [bruceadams/get-release](https://github.com/bruceadams/get-release) from 1.2.3 to 1.3.2.
- [Release notes](https://github.com/bruceadams/get-release/releases)
- [Commits](https://github.com/bruceadams/get-release/compare/v1.2.3...v1.3.2)

---
updated-dependencies:
- dependency-name: bruceadams/get-release
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-28 15:14:18 +02:00
Esteban Skallwar Blanc e9d4bb8dba Add CLI argument to disable SSL checks 2022-10-04 15:23:59 +02:00
Like0x 9282b5dee5 Update downloader.rs for Disable SSL checks 2022-10-04 15:23:59 +02:00
Esteban Skallwar Blanc ad4b89d18d misc: Update MSRV 2022-10-03 16:15:42 +02:00
Esteban Skallwar Blanc 62f17a236e misc: Update dependencies 2022-10-03 16:15:42 +02:00
Esteban Blanc 3fca2753b5 misc: Update MSRV 2022-08-01 22:40:05 +02:00
Esteban Blanc 20c4aa1346 misc: Update dependencies 2022-08-01 01:51:43 +02:00
Esteban Blanc 43c383a008 ci: Fix cross compilation for release workflow 2022-05-13 16:59:01 +02:00
Esteban Blanc 1b47539c71 ci: Add riscv64 build 2022-05-13 14:12:39 +02:00
Esteban Blanc 1f5e53f5eb misc: Revert to openssl
We switched some time ago to rustls in order to cross compile more
easilly but it depends on `ring` which in full assembly and does not the
future: RISC-V

This revert to openssl while making sure the CI still cross-compile
correctly to aarch64
2022-05-12 21:56:42 +02:00
Esteban Blanc c151757af6 ci: Build for aarch64 2022-05-11 10:47:57 +02:00
Orhun Parmaksız 3fb66dbffe ci: Build with --locked flag 2022-04-30 21:33:00 +02:00
Orhun Parmaksız 5e58e50a8e chore: Update Cargo.lock 2022-04-30 18:20:00 +02:00
Esteban Blanc 13e9d9f45d ci: Adjust MSRV 2022-04-29 20:51:35 +02:00
Esteban Blanc 18d16bffbd tls: Switch from openssl to rustls 2022-04-29 20:51:35 +02:00
Esteban Blanc 79cecf73d2 ci: Continue on error for release
The build is fails because of op
enssl-sys crate when cross compiling
2022-04-28 16:09:13 +02:00
Esteban Blanc fb124a5afb misc: Prepare for release
Bump crate version
Update README (MSRV and usage)
2022-04-28 13:27:05 +02:00
Esteban Blanc b467ee8a5e ci: Add riscv64 and aarch64 for release 2022-04-25 20:39:44 +02:00
Esteban Blanc c57fa1081c misc: Update dependencies in Cargo.lock 2022-04-25 17:27:08 +02:00
Esteban Blanc b09c9b487d misc: Fix all clippy warnings 2022-04-24 22:20:32 +02:00
dependabot[bot] 0171c62324 robo(ci): bump codecov/codecov-action from 2 to 3
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2 to 3.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-24 19:27:40 +02:00
raphTec a576ad9a9e Fix symlinks (relative link, argument order, folder creation)
Was completely broken basically:
- No parent directories were created
- The link target was wrong and not a local path
- The symlink function was called with source and target swapped (this is a documentation
  bug in the symlink crate tho)
2022-04-24 19:00:48 +02:00
raphTec 97e3a16f49 Add visit filter tests 2022-04-24 18:38:03 +02:00
raphTec 0d75eb68b9 Empty test directories before running tests
Otherwise a failed test leaves the directory populated, which fails future test runs.
2022-04-24 18:38:03 +02:00
raphTec ed56e9a078 Rename existing tests 2022-04-24 18:38:03 +02:00
raphTec 1ca0dcead8 Increase sleep duration 2022-04-24 18:38:03 +02:00
raphTec 62cb075e6c Add support for distinct dowload and visit regex
This allows fine-grained control whether a page is visited, that means its links analyzed,
and saved to disk.
The decoupling of download and visit filter means the complete website may still be
explored while only downloading some files. To speed up scraping, irrelevant links can be
easily excluded from visiting.
2022-04-24 18:38:03 +02:00
raphTec c6538e561d Fix lint warning (remove brackets) 2022-04-24 18:38:03 +02:00
raphTec 3a3f4784d2 Filter links before downloading / adding to the queue
This commit speeds up scraping for scenarios where pages have a high branch factor, that
is many links and a majority of these links is excluded by the --exclude / --include
rules.
This also improves memory usage in these scenarios since the links are not stored.
Also the network traffic is reduced by not downloading these links in the first place.
2022-04-24 18:38:03 +02:00
Esteban Blanc 84276b9631 ci: Stop dependabot PRs to the lockfile 2022-03-17 00:24:19 +01:00
dependabot[bot] 79f9614952
robo(deps): bump reqwest from 0.11.9 to 0.11.10 (#179)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.9 to 0.11.10.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.9...v0.11.10)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-15 08:48:24 +01:00
dependabot[bot] c4cd36b98f
robo(deps): bump regex from 1.5.4 to 1.5.5 (#174)
Bumps [regex](https://github.com/rust-lang/regex) from 1.5.4 to 1.5.5.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.5.4...1.5.5)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-09 09:08:50 +01:00
dependabot[bot] 2d2e747db8 robo(ci): bump actions/upload-artifact from 2.3.1 to 3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2.3.1 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2.3.1...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-05 11:16:26 +01:00
dependabot[bot] a24000f900 robo(ci): bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-04 21:37:25 +01:00
Esteban Blanc 23576977ad
Fix clippy warnings and add clippy ci check (#168)
* misc: Run clippy and fixes all warnings

* ci: Add clippy check
2022-02-28 09:51:23 +01:00
dependabot[bot] 422b810a78
robo(deps): bump rand from 0.8.4 to 0.8.5 (#169)
Bumps [rand](https://github.com/rust-random/rand) from 0.8.4 to 0.8.5.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.8.4...0.8.5)

---
updated-dependencies:
- dependency-name: rand
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-28 09:32:35 +01:00
dependabot[bot] f6072a9bee
robo(deps): bump tiny_http from 0.10.0 to 0.11.0 (#171)
Bumps [tiny_http](https://github.com/tiny-http/tiny-http) from 0.10.0 to 0.11.0.
- [Release notes](https://github.com/tiny-http/tiny-http/releases)
- [Changelog](https://github.com/tiny-http/tiny-http/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tiny-http/tiny-http/compare/v0.10.0...0.11.0)

---
updated-dependencies:
- dependency-name: tiny_http
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-28 09:08:37 +01:00
dependabot[bot] 931f1e5131 robo(deps): bump structopt from 0.3.25 to 0.3.26
Bumps [structopt](https://github.com/TeXitoi/structopt) from 0.3.25 to 0.3.26.
- [Release notes](https://github.com/TeXitoi/structopt/releases)
- [Changelog](https://github.com/TeXitoi/structopt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TeXitoi/structopt/compare/v0.3.25...v0.3.26)

---
updated-dependencies:
- dependency-name: structopt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-19 11:54:31 +01:00
dependabot[bot] a999137d31
robo(deps): bump tiny_http from 0.9.0 to 0.10.0 (#165)
Bumps [tiny_http](https://github.com/tiny-http/tiny-http) from 0.9.0 to 0.10.0.
- [Release notes](https://github.com/tiny-http/tiny-http/releases)
- [Changelog](https://github.com/tiny-http/tiny-http/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tiny-http/tiny-http/compare/0.9.0...v0.10.0)

---
updated-dependencies:
- dependency-name: tiny_http
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-13 13:08:04 +01:00
dependabot[bot] 52d372372e
robo(deps): bump encoding_rs from 0.8.29 to 0.8.30 (#160)
Bumps [encoding_rs](https://github.com/hsivonen/encoding_rs) from 0.8.29 to 0.8.30.
- [Release notes](https://github.com/hsivonen/encoding_rs/releases)
- [Commits](https://github.com/hsivonen/encoding_rs/compare/v0.8.29...v0.8.30)

---
updated-dependencies:
- dependency-name: encoding_rs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-12 10:03:55 +01:00
dependabot[bot] 9330231522
robo(deps): bump reqwest from 0.11.8 to 0.11.9 (#164)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.8 to 0.11.9.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.8...v0.11.9)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-11 16:02:17 +01:00
dependabot[bot] ed119f15dd robo(ci): bump actions/upload-artifact from 2.2.4 to 2.3.1
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2.2.4 to 2.3.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2.2.4...v2.3.1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-21 16:46:35 +01:00
dependabot[bot] f78632c7ee
robo(deps): bump reqwest from 0.11.6 to 0.11.8 (#163)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.6 to 0.11.8.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.6...v0.11.8)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-21 11:24:30 +01:00
Esteban Blanc 2f684793be misc: Enable lto 2021-11-18 12:32:07 +01:00
Esteban Blanc 7f6eb7ca37 misc: Update dependencies 2021-10-19 12:06:09 +02:00
dependabot[bot] 1f3a6cbcf7
robo(deps): bump tiny_http from 0.8.2 to 0.9.0 (#154)
Bumps [tiny_http](https://github.com/tiny-http/tiny-http) from 0.8.2 to 0.9.0.
- [Release notes](https://github.com/tiny-http/tiny-http/releases)
- [Changelog](https://github.com/tiny-http/tiny-http/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tiny-http/tiny-http/compare/0.8.2...0.9.0)

---
updated-dependencies:
- dependency-name: tiny_http
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-15 12:43:16 +02:00
dependabot[bot] 399351261c robo(deps): bump reqwest from 0.11.4 to 0.11.5
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.4 to 0.11.5.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.4...v0.11.5)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-08 10:17:43 +02:00
dependabot[bot] 1386a4b66b robo(deps): bump pathdiff from 0.2.0 to 0.2.1
Bumps [pathdiff](https://github.com/Manishearth/pathdiff) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/Manishearth/pathdiff/releases)
- [Commits](https://github.com/Manishearth/pathdiff/commits)

---
updated-dependencies:
- dependency-name: pathdiff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-04 21:39:24 +02:00
Lucas eebbf5647d Split html charset tests 2021-10-04 20:56:03 +02:00
Lucas 4255ef29c2 Add tests 2021-10-04 20:56:03 +02:00
lhvy 034f286638 Fix <meta charset> with single quotes 2021-10-04 20:56:03 +02:00
dependabot[bot] c14cb0827f robo(ci): bump actions/upload-artifact from 1 to 2.2.4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 2.2.4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v1...v2.2.4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-23 20:56:28 +02:00
Esteban Blanc 04edd27550 Add tests for external download 2021-09-23 20:43:54 +02:00
marchellodev ef79cc2318 Add external site downloading support
Introduces --edepth flag for external domain depth
Transforms urls that start with `//` into full https links for more accurate detection of external links
Fixes panic when url starts with `///`
Readme: Added explanation of the `--edepth` flag
--edepth -> --ext-depth
Refactors code to avoid repetition (normalizing urls)
Improved code documentation (url normalization)
README.md upd: --ext-depth
Minor docs changes (--depth)
rustfmt coding style
Updated `--ext_depth` command docs
Small code documentation improvements

Co-authored-by: CohenArthur <cohenarthur.dev@gmail.com>
Co-authored-by: Esteban Blanc <estblcsk@gmail.com>
2021-09-23 20:43:54 +02:00
Esteban Blanc 1be1f856e9 ci: Add codecov 2021-09-10 14:39:21 +02:00
dependabot[bot] caa0dedc00
robo(ci): bump bruceadams/get-release from 1.2.2 to 1.2.3 (#147)
Bumps [bruceadams/get-release](https://github.com/bruceadams/get-release) from 1.2.2 to 1.2.3.
- [Release notes](https://github.com/bruceadams/get-release/releases)
- [Commits](https://github.com/bruceadams/get-release/compare/v1.2.2...v1.2.3)

---
updated-dependencies:
- dependency-name: bruceadams/get-release
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-09 11:55:21 +02:00
dependabot[bot] 9cb0b81286
robo(deps): bump structopt from 0.3.22 to 0.3.23 (#148)
Bumps [structopt](https://github.com/TeXitoi/structopt) from 0.3.22 to 0.3.23.
- [Release notes](https://github.com/TeXitoi/structopt/releases)
- [Changelog](https://github.com/TeXitoi/structopt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TeXitoi/structopt/compare/v0.3.22...v0.3.23)

---
updated-dependencies:
- dependency-name: structopt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-31 23:12:46 +02:00
pinkforest(she/her) 729c737c50
Lib bin initial (#141)
* Renames main binary to new lib-bin tree

* Initialises base lib-bin structure
2021-07-07 23:39:32 +02:00
Esteban Blanc 7c05a181e0 Bump version for v0.1.2 release (#138) 2021-07-07 19:20:24 +02:00
mr-bo-jangles 0b1a556ce1 Lets make this non-unix specific for our windows based friends (#134)
Co-authored-by: Esteban Blanc <estblcsk@gmail.com>
2021-07-07 19:20:24 +02:00
Esteban Blanc e25e358f35 misc: Add MSRV (#137) 2021-07-07 19:20:24 +02:00
pinkforest(she/her) f87f6bf078 Stops freezing Firefoxes looking into the repo (#139)
* Fixes medias for release

* SVG Change from 15 rows to 40 rows with 100ms frames
2021-07-07 19:20:24 +02:00
Esteban Blanc de543179c0 ci: Fix same id issue in release workflow (#140) 2021-07-07 19:20:16 +02:00
dependabot[bot] 2c08ae00f4
robo(deps): bump structopt from 0.3.21 to 0.3.22 (#131)
Bumps [structopt](https://github.com/TeXitoi/structopt) from 0.3.21 to 0.3.22.
- [Release notes](https://github.com/TeXitoi/structopt/releases)
- [Changelog](https://github.com/TeXitoi/structopt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TeXitoi/structopt/compare/v0.3.21...v0.3.22)

---
updated-dependencies:
- dependency-name: structopt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-05 09:33:37 +02:00
dependabot[bot] d388b3c3f2
robo(deps): bump crossbeam from 0.8.0 to 0.8.1 (#126)
Bumps [crossbeam](https://github.com/crossbeam-rs/crossbeam) from 0.8.0 to 0.8.1.
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-0.8.0...crossbeam-0.8.1)

---
updated-dependencies:
- dependency-name: crossbeam
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-02 11:06:08 +02:00
dependabot[bot] ef8518e877
robo(deps): bump regex from 1.4.6 to 1.5.4 (#128)
Bumps [regex](https://github.com/rust-lang/regex) from 1.4.6 to 1.5.4.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.4.6...1.5.4)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-02 10:53:17 +02:00
dependabot[bot] ed7a22876a
robo(deps): bump url from 2.2.0 to 2.2.2 (#127)
Bumps [url](https://github.com/servo/rust-url) from 2.2.0 to 2.2.2.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.2.0...v2.2.2)

---
updated-dependencies:
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-02 10:29:53 +02:00
dependabot[bot] f0c5448d1d
robo(deps): bump reqwest from 0.11.0 to 0.11.4 (#125)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.0 to 0.11.4.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.0...v0.11.4)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-02 10:19:53 +02:00
pinkforest(she/her) a7f4334910
Adds badges (#122)
* * Adds badges

* Fixes mistake on badge
2021-07-02 09:47:44 +02:00
pinkforest(she/her) 1e034011a7
Updated dependencies and dependabot (#123)
* Create dependabot.yml

* robo(deps): bump rand from 0.8.1 to 0.8.4

Bumps [rand](https://github.com/rust-random/rand) from 0.8.1 to 0.8.4.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.8.1...0.8.4)

---
updated-dependencies:
- dependency-name: rand
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* robo(deps): bump subprocess from 0.2.6 to 0.2.7

Bumps [subprocess](https://github.com/hniksic/rust-subprocess) from 0.2.6 to 0.2.7.
- [Release notes](https://github.com/hniksic/rust-subprocess/releases)
- [Commits](https://github.com/hniksic/rust-subprocess/compare/release/0.2.6...release/0.2.7)

---
updated-dependencies:
- dependency-name: subprocess
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* robo(deps): bump encoding_rs from 0.8.26 to 0.8.28

Bumps [encoding_rs](https://github.com/hsivonen/encoding_rs) from 0.8.26 to 0.8.28.
- [Release notes](https://github.com/hsivonen/encoding_rs/releases)
- [Commits](https://github.com/hsivonen/encoding_rs/compare/v0.8.26...v0.8.28)

---
updated-dependencies:
- dependency-name: encoding_rs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* robo(deps): bump tiny_http from 0.7.0 to 0.8.2

Bumps [tiny_http](https://github.com/tiny-http/tiny-http) from 0.7.0 to 0.8.2.
- [Release notes](https://github.com/tiny-http/tiny-http/releases)
- [Changelog](https://github.com/tiny-http/tiny-http/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tiny-http/tiny-http/compare/0.7.0...0.8.2)

---
updated-dependencies:
- dependency-name: tiny_http
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* robo(deps): bump regex from 1.4.3 to 1.4.6

Bumps [regex](https://github.com/rust-lang/regex) from 1.4.3 to 1.4.6.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.4.3...1.4.6)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-02 09:19:30 +02:00
Markus Unterwaditzer 9a194d784e
Fix scraping on URLs that have fragments (#120) 2021-05-25 21:47:55 +02:00
Markus Unterwaditzer dcb5080fca
Fix flipped continue_on_error check (#118) 2021-05-24 14:03:58 +02:00
Markus Unterwaditzer 422534f146
Fix querystring support (#117)
* Fix querystring support

* While technically valid filenames, having ? in filenames means that the directory structure cannot be served by nginx as the resulting filenames cannot be addressed via URLs unless the ? is percent-encoded in every link.

* Query strings can contain trailing slashes. If that happens, suckit will attempt to create a directory, then create a same-named file, which fails

* fix fmt
2021-05-24 10:10:45 +02:00
Bastien Germond 330829913e
logger: fix panic! changes for Rust 2021 (#115) 2021-05-20 21:49:36 +02:00
Esteban Blanc 94bbc9986d
Minor Fixes (#112)
* scraper: Fix charset regex

* scraper: Add unsuported charset warning

* scraper: Log error or warn on multiple download failed attempts
2021-01-14 22:11:36 +01:00
Esteban Blanc 1bdb086651
Patches for #110 (#111)
* scrapper: Log an error if an url is not in the path map

* url_helper: Fix domain/1/ vs domain/1 file or dir conflict

* url_helper: Fix to_path()
2021-01-14 14:08:35 +01:00
Esteban Blanc 6090e4a538 misc: Bump version number 2021-01-11 21:58:35 +01:00
Esteban Blanc 96e1f2bf52
Respect website architecture (#110)
* scraper: Stop flat archive creation

* scrapper: Fix_domtree create only relative path

* tests: Fix to_path() tests

* tests: Fix tests

* scraper: Fix doc

Co-authored-by: CohenArthur <arthur.cohen@epita.fr>

* misc: Cleanup

* Update tests/fixtures/mod.rs

Co-authored-by: CohenArthur <arthur.cohen@epita.fr>

Co-authored-by: CohenArthur <arthur.cohen@epita.fr>
2021-01-10 22:55:37 +01:00
CohenArthur 299247a107
Use local webserver for speed testing (#108)
* tests: Add script to set up local server

* tests: Remove old results from speed.csv since test methodology changes

* tests: Change speed_regression so it downloads locally hosted webserver

* local_server_setup: Remove need of sudo since suckit understands ports

* speed_regression: Start local web server in script

Co-authored-by: Esteban Blanc <estblcsk@gmail.com>
2021-01-06 11:10:38 +01:00
Esteban Blanc 56c3970edd Merge branch 'master' of github.com:Skallwar/suckit 2021-01-05 12:02:09 +01:00
Esteban Blanc 07df03fd07 ci: Add sha512 checksum 2021-01-05 12:01:45 +01:00
Esteban Blanc 1819a20ed5
Update README.md 2021-01-05 11:04:33 +01:00
Esteban Blanc db3c933423
Support other charset (#105)
* scraper: Initial work for other charset support

Not working

* scraper: Get charset from html instead of http headers

Not working

* scraper: Charset working

* scraper: Fix regex ('"' after equals)

* scraper: Use lazy_static for charset regex

* misc: Refactor pr

* tests: Refactor fixtures

* tests: Add html charset support tests

* tests: Charset, split tests

* downloader: Get charset from http headers

* misc: Clarify iterator first

* Apply suggestions from CohenArthur

Co-authored-by: CohenArthur <arthur.cohen@epita.fr>
2021-01-05 09:57:12 +01:00
Esteban Blanc d26647c17d ci: Use action for release info 2020-12-22 11:17:48 +01:00
Esteban Blanc 1ddbe1702e ci: Add release build (#106)
* ci: Build target aware

* ci: Add release.yml
2020-12-22 10:56:13 +01:00
Esteban Blanc 09af371589
misc: Update dependencies (#103) 2020-12-19 16:38:26 +01:00
orhun 9adfe8bc1f
readme: Update README about installation from AUR (#101) 2020-10-12 20:56:20 +02:00
Patrick Sier fa549f2fed
Remove hash fragment from scraped URLs (#100)
* refactor: remove hash fragment from scraped urls

* refactor: handle fragments in download paths

* test: split url_to_path tests
2020-10-12 16:17:50 +02:00
orhun d9c17d9ec2
fix: Use CARGO_BIN_EXE instead of hardcoded path for tests (#98) 2020-10-12 13:43:10 +02:00
CohenArthur 5705175132
readme: Update README with latest additions (#97)
The README lacked the latest features added to suckit, such as random delays between downloads or user authentication
2020-10-12 10:14:46 +02:00
Patrick Sier f3f19fd1e8
Add support for HTTP basic auth args (#96)
* feat: add arg for HTTP basic auth

* feat: chunk space-separated auth args

* refactor: update auth hashmap, host handling

Require host for auth HashMap, defaulting to the origin URL's host value
if not provided otherwise.

* refactor: auth help string, constant updates

* test: add auth functional tests
2020-10-12 09:17:44 +02:00
Jaslo Ziska 794d8ddd53
Add dry run flag (#95)
Add --dry-run flag to stop files from being saved, as mentioned in
https://github.com/Skallwar/suckit/issues/94
2020-10-03 22:36:35 +02:00
Esteban Blanc ffec62254d
cargo: Add caret requirements (#93) 2020-08-31 16:52:40 +02:00
40 changed files with 2933 additions and 875 deletions

20
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,20 @@
version: 2
updates:
# ATM this only create pr to the lockfile
# - package-ecosystem: "cargo"
# directory: "/"
# schedule:
# interval: "daily"
# time: "02:00" # UTC
# labels:
# - "domain: deps"
# commit-message:
# prefix: "robo(deps)"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "domain: ci"
commit-message:
prefix: "robo(ci)"

46
.github/env.sh vendored Executable file
View file

@ -0,0 +1,46 @@
#!/bin/bash
set -e
if [ $# != 1 ]; then
echo "Usage: . $0 <rust-target>"
exit 1
fi
arch=""
sysroot=""
need_target_linker=""
ubuntu_cross_pkg_list=""
target_linker=""
target_cc=""
case "$1" in
aarch64*)
arch="arm64"
sysroot="/usr/lib/aarch64-linux-gnu/"
ubuntu_cross_pkg_list="gcc-aarch64-linux-gnu"
# YES they are the same but otherwise it fails
target_linker="aarch64-linux-gnu-gcc"
target_cc="aarch64-linux-gnu-gcc"
;;
riscv64*)
arch="riscv64"
sysroot="/usr/lib/riscv64-linux-gnu/"
ubuntu_cross_pkg_list="gcc-riscv64-linux-gnu"
# YES they are the same but otherwise it fails
target_linker="riscv64-linux-gnu-gcc"
target_cc="riscv64-linux-gnu-gcc"
;;
x86_64*)
target_linker="gcc"
target_cc="gcc"
;;
esac
echo MULTILIB_ARCH=$arch
echo PKG_CONFIG_SYSROOT_DIR=$sysroot
echo TARGET_CC=$target_cc
echo CARGO_TARGET_$(echo "$1" | tr 'a-z' 'A-Z' | tr '-' '_' )_LINKER=$target_linker
echo UBUNTU_CROSS_PKG_LIST=$ubuntu_cross_pkg_list

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Lint
run: |
rustup component add rustfmt
@ -21,23 +21,106 @@ jobs:
fmt' (version $(cargo fmt --version))"; false)
tests:
runs-on: ubuntu-latest
needs: [lint]
name: Tests ${{ matrix.target }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
# - name: Set environment variables
# run: |
# .github/env.sh ${{ matrix.target }} >> $GITHUB_ENV
# - name: Install qemu-user-static
# if: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
# run: |
# sudo apt update
# sudo apt install qemu-user-static
# - uses: ryankurte/action-apt@v0.3.0
# if: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
# with:
# arch: ${{ env.MULTILIB_ARCH }}
# packages: "libssl-dev:${{ env.MULTILIB_ARCH }}"
- name: Rustup setup
uses: actions-rs/toolchain@v1
with:
toolchain: stable
default: true
target: ${{ matrix.target }}
- name: Unit tests
run: |
cargo test
cargo test --locked
build:
runs-on: ubuntu-latest
needs: [lint, tests]
needs: [lint]
name: Build ${{ matrix.target }}-${{ matrix.rust }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
- riscv64gc-unknown-linux-gnu
rust:
- stable
- 1.70.0
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Rustup setup
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
default: true
target: ${{ matrix.target }}
- name: Set environment variables
run: |
.github/env.sh ${{ matrix.target }} >> $GITHUB_ENV
- name: Install libssl (native)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
run: |
sudo apt update
sudo apt install libssl-dev
# - name: Install libssl (cross)
# uses: ryankurte/action-apt@v0.3.0
# if: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
# with:
# arch: ${{ env.MULTILIB_ARCH }}
# packages: "${{ env.UBUNTU_CROSS_PKG_LIST }} libssl-dev:${{ env.MULTILIB_ARCH }}"
- name: Install libssl (cross)
if: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
run: |
sudo dpkg --add-architecture ${{ env.MULTILIB_ARCH }}
sudo sed -i'' -E 's/^(deb|deb-src) /\1 [arch=amd64,i386] /' /etc/apt/sources.list
echo "deb [arch=${{ env.MULTILIB_ARCH }}] http://ports.ubuntu.com/ubuntu-ports/ $(lsb_release -cs) main restricted universe" | sudo tee /etc/apt/sources.list.d/${{ env.MULTILIB_ARCH }}.list
echo "deb [arch=${{ env.MULTILIB_ARCH }}] http://ports.ubuntu.com/ubuntu-ports/ $(lsb_release -cs)-updates main restricted universe" | sudo tee -a /etc/apt/sources.list.d/${{ env.MULTILIB_ARCH }}.list
echo "deb [arch=${{ env.MULTILIB_ARCH }}] http://ports.ubuntu.com/ubuntu-ports/ $(lsb_release -cs)-security main restricted universe" | sudo tee -a /etc/apt/sources.list.d/${{ env.MULTILIB_ARCH }}.list
sudo apt update
sudo apt install ${{ env.UBUNTU_CROSS_PKG_LIST }} libssl-dev:${{ env.MULTILIB_ARCH }}
#- name: Setup tmate session
#uses: mxschmitt/action-tmate@v3
- name: Check
run: |
cargo check
run: cargo check --locked --target ${{ matrix.target }}
- name: Build
run: |
cargo build
run: cargo build --locked --target ${{ matrix.target }}

27
.github/workflows/clippy.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Clippy
on:
pull_request:
branches:
- master
push:
branches:
- master
jobs:
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Rustup setup
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features

37
.github/workflows/coverage.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: code-coverage
on:
pull_request:
branches:
- master
push:
branches:
- master
jobs:
check:
name: Rust project
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Run tarpaulin
run: |
cargo install cargo-tarpaulin
cargo tarpaulin --engine llvm --out Xml -- --test-threads 1
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
- name: Archive code coverage results
uses: actions/upload-artifact@v4
with:
name: code-coverage-report
path: cobertura.xml

78
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,78 @@
name: Release upload artifact
on:
release:
types: [published, edited]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
target:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
- riscv64gc-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
- name: Rustup setup
uses: actions-rs/toolchain@v1
with:
toolchain: stable
default: true
target: ${{ matrix.target }}
- name: Set environment variables
run: |
.github/env.sh ${{ matrix.target }} >> $GITHUB_ENV
- name: Install libssl (native)
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
run: |
sudo apt update
sudo apt install libssl-dev
- name: Install libssl (cross)
uses: ryankurte/action-apt@master
if: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }}
with:
arch: ${{ env.MULTILIB_ARCH }}
packages: "${{ env.UBUNTU_CROSS_PKG_LIST }} libssl-dev:${{ env.MULTILIB_ARCH }}"
- name: Get release upload url
id: get_release
uses: bruceadams/get-release@v1.3.2
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Build release
run: |
cargo build --release --locked --target ${{ matrix.target }}
- name: Compute sha512
run: |
sha512sum ./target/${{ matrix.target }}/release/suckit | cut -d " " -f 1 > suckit.sha512
- name: Upload release asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url}}
asset_path: ./target/${{ matrix.target }}/release/suckit
asset_name: suckit-${{ steps.get_release.outputs.tag_name }}-${{ matrix.target }}
asset_content_type: application/x-elf
- name: Upload release asset checksum
id: upload-release-asset-sha512
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url}}
asset_path: ./suckit.sha512
asset_name: suckit-${{ steps.get_release.outputs.tag_name }}-${{ matrix.target }}.sha512
asset_content_type: text/plain

3
.gitignore vendored
View file

@ -6,3 +6,6 @@
tags
.idea/
# Ignore local web server folder
tests/local_server

1584
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
[package]
name = "suckit"
version = "0.1.0"
version = "0.2.0"
edition = "2018"
authors = ["Esteban \"Skallwar\" Blanc <estblcsk@gmail.com>",
"Arthur \"CohenArthur\" Cohen <arthur.cohen@epita.fr>"]
"Arthur \"CohenArthur\" Cohen <cohenarthur.dev@gmail.com>"]
license = "MIT OR Apache-2.0"
homepage = "https://github.com/skallwar/suckit"
repository = "https://github.com/skallwar/suckit"
@ -19,21 +19,39 @@ include = [
"src/*",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[package.metadata]
msrv = "1.67.0"
[lib]
name = "suckit"
path = "src/lib.rs"
[[bin]]
name = "suckit"
path = "src/bin/suckit.rs"
doc = false
[dependencies]
structopt = "0.3"
crossbeam = "0.7"
reqwest = { version = "0.10", features = ["blocking", "cookies"] }
pretty_assertions = "0.6"
kuchiki = "0.8"
colored = "1.9"
chrono = "0.4"
bytes = "0.5"
percent-encoding = "2.1"
url = "2.1"
rand = "0.7"
regex = "1.3.7"
structopt = "^0.3"
crossbeam = "^0.8"
reqwest = { version = "^0.11", features = ["blocking", "cookies"] }
kuchiki = "^0.8"
colored = "2.0"
chrono = "^0.4"
url = "^2.3"
rand = "^0.8"
regex = "^1.6"
encoding_rs = "^0.8"
lazy_static = "1.4.0"
pathdiff = "^0.2"
md5 = "^0.7"
symlink = "^0.1.0"
[dev-dependencies]
tiny_http = "0.7.0"
tiny_http = "^0.12"
subprocess = "^0.2"
mktemp = "^0.5"
portpicker = "^0.1"
[profile.release]
lto = true

View file

@ -1,11 +1,18 @@
![Build and test](https://github.com/Skallwar/suckit/workflows/Build%20and%20test/badge.svg)
[![codecov](https://codecov.io/gh/Skallwar/suckit/branch/master/graph/badge.svg?token=ZLD369AY2G)](https://codecov.io/gh/Skallwar/suckit)
[![Crates.io](https://img.shields.io/crates/v/suckit.svg)](https://crates.io/crates/suckit)
[![Docs](https://docs.rs/suckit/badge.svg)](https://docs.rs/suckit)
[![Deps](https://deps.rs/repo/github/Skallwar/suckit/status.svg)](https://deps.rs/repo/github/Skallwar/suckit)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![MSRV](https://img.shields.io/badge/MSRV-1.70.0-blue)
# SuckIT
`SuckIT` allows you to recursively visit and download a website's content to
your disk.
![SuckIT Logo](suckit_logo.png)
![SuckIT Logo](media/suckit_logo.png)
# Features
@ -13,22 +20,62 @@ your disk.
* [x] Uses multithreading
* [x] Writes the website's content to your disk
* [x] Enables offline navigation
* [x] Offers random delays to avoid IP banning
* [ ] Saves application state on CTRL-C for later pickup
* [ ] Offers random delays to avoid IP banning
# Options
```console
USAGE:
suckit [FLAGS] [OPTIONS] <url>
|Option|Behavior|
|---|---|
|`-h, --help`|Displays help information|
|`-v, --verbose`|Activate Verbose output|
|`-d, --depth`|Specify the level of depth to go to when visiting the website|
|`-j, --jobs`|Number of threads to use|
|`-o, --output`|Output directory where the downloaded files are written|
|`-t, --tries`|Number of times to retry when the downloading of a page fails|
|`-u, --user-agent`|User agent to be used for sending requests|
|`-i, --include`|Specify a regex to include pages that match this pattern|
|`-e, --exclude`|Specify a regex to exclude pages that match this pattern|
FLAGS:
-c, --continue-on-error Flag to enable or disable exit on error
--disable-certs-checks Dissable SSL certificates verification
--dry-run Do everything without saving the files to the disk
-h, --help Prints help information
-V, --version Prints version information
-v, --verbose Enable more information regarding the scraping process
--visit-filter-is-download-filter Use the dowload filter in/exclude regexes for visiting as well
OPTIONS:
-a, --auth <auth>...
HTTP basic authentication credentials space-separated as "username password host". Can be repeated for
multiple credentials as "u1 p1 h1 u2 p2 h2"
--cookie <cookie>
Cookie to send with each request, format: key1=value1;key2=value2 [default: ]
--delay <delay>
Add a delay in seconds between downloads to reduce the likelihood of getting banned [default: 0]
-d, --depth <depth>
Maximum recursion depth to reach when visiting. Default is -1 (infinity) [default: -1]
-e, --exclude-download <exclude-download>
Regex filter to exclude saving pages that match this expression [default: $^]
--exclude-visit <exclude-visit>
Regex filter to exclude visiting pages that match this expression [default: $^]
--ext-depth <ext-depth>
Maximum recursion depth to reach when visiting external domains. Default is 0. -1 means infinity [default:
0]
-i, --include-download <include-download>
Regex filter to limit to only saving pages that match this expression [default: .*]
--include-visit <include-visit>
Regex filter to limit to only visiting pages that match this expression [default: .*]
-j, --jobs <jobs> Maximum number of threads to use concurrently [default: 1]
-o, --output <output> Output directory
--random-range <random-range>
Generate an extra random delay between downloads, from 0 to this number. This is added to the base delay
seconds [default: 0]
-t, --tries <tries> Maximum amount of retries on download failure [default: 20]
-u, --user-agent <user-agent> User agent to be used for sending requests [default: suckit]
ARGS:
<url> Entry point of the scraping
```
# Example
@ -36,7 +83,7 @@ A common use case could be the following:
`suckit http://books.toscrape.com -j 8 -o /path/to/downloaded/pages/`
[![asciicast](https://asciinema.org/a/327889.svg)](https://asciinema.org/a/327889)
![asciicast](media/suckit-adjusted-120cols-40rows-100ms.svg)
# Installation
@ -52,6 +99,14 @@ instructions on how to install Rust.
* Now, run it from anywhere with the `suckit` command.
### Arch Linux
`suckit` can be installed from available [AUR packages](https://aur.archlinux.org/packages/?O=0&SeB=b&K=suckit&outdated=&SB=n&SO=a&PP=50&do_Search=Go) using an [AUR helper](https://wiki.archlinux.org/index.php/AUR_helpers). For example,
```
yay -S suckit
```
__Want to contribute ? Feel free to
[open an issue](https://github.com/Skallwar/suckit/issues/new) or
[submit a PR](https://github.com/Skallwar/suckit/compare) !__

130
flake.lock Normal file
View file

@ -0,0 +1,130 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1705774713,
"narHash": "sha256-j6ADaDH9XiumUzkTPlFyCBcoWYhO83lfgiSqEJF2zcs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1b64fc1287991a9cce717a01c1973ef86cb1af0b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1705889935,
"narHash": "sha256-77KPBK5e0ACNzIgJDMuptTtEqKvHBxTO3ksqXHHVO+4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "e36f66bb10b09f5189dc3b1706948eaeb9a1c555",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

29
flake.nix Normal file
View file

@ -0,0 +1,29 @@
{
description = "SuckIT devshell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
in
with pkgs;
{
devShell = mkShell {
buildInputs = [
rust-bin.stable.latest.default
openssl
pkg-config
];
};
}
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

View file

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 265 KiB

View file

@ -33,10 +33,17 @@ pub struct Args {
short,
long,
default_value = "-1",
help = "Maximum recursion depth to reach when visiting. -1 is the default and will go as far as it can"
help = "Maximum recursion depth to reach when visiting. Default is -1 (infinity)"
)]
pub depth: i32,
#[structopt(
long,
default_value = "0",
help = "Maximum recursion depth to reach when visiting external domains. Default is 0. -1 means infinity"
)]
pub ext_depth: i32,
///Number of retries when downloading a page fails
#[structopt(
short,
@ -79,29 +86,79 @@ pub struct Args {
)]
pub user_agent: String,
/// Regex filter to limit saving pages to only matched ones
/// Cookie header
#[structopt(
short,
long,
default_value = ".*",
parse(try_from_str = parse_regex),
help = "Regex filter to limit to only saving pages that match this expression"
default_value = "",
help = "Cookie to send with each request, format: key1=value1;key2=value2"
)]
pub include: Regex,
pub cookie: String,
/// Regex filter to limit visiting pages to only matched ones
#[structopt(
long,
default_value = ".*",
parse(try_from_str = parse_regex),
help = "Regex filter to limit to only visiting pages that match this expression"
)]
pub include_visit: Regex,
/// Regex filter to limit visiting pages to only matched ones
#[structopt(
long,
default_value = "$^",
parse(try_from_str = parse_regex),
help = "Regex filter to exclude visiting pages that match this expression"
)]
pub exclude_visit: Regex,
/// Regex filter to limit saving pages to only matched ones
#[structopt(
short,
long,
default_value = ".*",
parse(try_from_str = parse_regex),
help = "Regex filter to limit to only saving pages that match this expression"
)]
pub include_download: Regex,
/// Regex filter to limit saving pages to only matched ones
#[structopt(
short,
long,
default_value = "$^",
parse(try_from_str = parse_regex),
help = "Regex filter to exclude saving pages that match this expression"
)]
pub exclude_download: Regex,
/// If set, set the visit filter to the values of the download filter
#[structopt(
long,
help = "Use the dowload filter in/exclude regexes for visiting as well"
)]
pub visit_filter_is_download_filter: bool,
/// HTTP basic authentication credentials
#[structopt(
short,
long,
default_value = "$^",
parse(try_from_str = parse_regex),
help = "Regex filter to exclude saving pages that match this expression"
use_delimiter = true,
value_delimiter = " ",
help = "HTTP basic authentication credentials space-separated as \"username password host\". Can be repeated for multiple credentials as \"u1 p1 h1 u2 p2 h2\""
)]
pub exclude: Regex,
pub auth: Vec<String>,
/// Decides if we should bail out on download error (like, too many redirects)
#[structopt(short, long, help = "Flag to enable or disable exit on error")]
pub continue_on_error: bool,
/// If set, run without saving anything to the disk
#[structopt(long, help = "Do everything without saving the files to the disk")]
pub dry_run: bool,
#[structopt(long, help = "Dissable SSL certificates verification")]
pub disable_certs_checks: bool,
}
impl Args {

10
src/bin/suckit.rs Normal file
View file

@ -0,0 +1,10 @@
use suckit::args::Args;
use suckit::scraper::Scraper;
fn main() {
let args = Args::collect();
let mut scraper = Scraper::new(args);
scraper.run();
}

View file

@ -1,7 +1,11 @@
use pathdiff;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use symlink::symlink_file;
use crate::{error, warn};
///Save content in a file
@ -12,11 +16,8 @@ pub fn save_file(file_name: &str, content: &[u8], path: &Option<PathBuf>) {
};
if let Some(parent) = path.parent() {
match fs::create_dir_all(parent) {
Err(err) => {
error!("Couldn't create folder {}: {}", parent.display(), err);
}
Ok(()) => (),
if let Err(err) = fs::create_dir_all(parent) {
error!("Couldn't create folder {}: {}", parent.display(), err);
}
}
@ -32,16 +33,33 @@ pub fn save_file(file_name: &str, content: &[u8], path: &Option<PathBuf>) {
///Create a symlink
pub fn symlink(source: &str, destination: &str, path: &Option<PathBuf>) {
let source = match path {
Some(path) => path.join(source),
None => PathBuf::from(source),
};
if let Some(parent) = source.parent() {
match fs::create_dir_all(parent) {
Err(err) => {
error!("Couldn't create folder {}: {}", parent.display(), err);
}
Ok(()) => (),
}
}
let destination = match path {
Some(path) => path.join(destination),
None => PathBuf::from(destination),
};
if let Err(_) = std::os::unix::fs::symlink(source, &destination) {
let target = pathdiff::diff_paths(&destination, &source.parent().unwrap()).unwrap();
if let Err(err) = symlink_file(&target, &source) {
warn!(
"{} is already present, coulnd't create a symlink to {}",
destination.display(),
source,
"Couldn't create symlink\n{} -> {}:\n{:#?}",
source.display(),
target.display(),
err,
);
}
}

View file

@ -1,24 +1,88 @@
use super::response::{Response, ResponseData};
use std::collections::HashMap;
use lazy_static::lazy_static;
use regex::Regex;
use reqwest::header::{HeaderMap, HeaderValue, COOKIE};
use url::Url;
use crate::warn;
use super::response::{Response, ResponseData};
const AUTH_CHUNK_SIZE: usize = 3;
///A Downloader to download web content
pub struct Downloader {
client: reqwest::blocking::Client,
tries: usize,
auth_map: HashMap<String, (String, Option<String>)>,
}
/// Parse HTTP authentication credentials from string iterable
fn parse_auth(auth: &[String], origin: &Url) -> Result<(String, Option<String>, String), String> {
// Convert any empty strings to None
let auth: Vec<Option<String>> = auth
.iter()
.map(|s| match s.as_ref() {
"" => None,
s => Some(s.to_string()),
})
.collect();
// Match on auth values and origin host, defaulting to the origin host if host not provided
match (auth.as_slice(), origin.host_str()) {
([Some(username)], Some(origin_host)) => {
Ok((username.to_string(), None, origin_host.to_string()))
}
([Some(username), password], Some(origin_host)) => Ok((
username.to_string(),
password.clone(),
origin_host.to_string(),
)),
([Some(username), password, None, ..], Some(origin_host)) => Ok((
username.to_string(),
password.clone(),
origin_host.to_string(),
)),
([Some(username), password, Some(host), ..], _) => {
Ok((username.to_string(), password.clone(), host.to_string()))
}
_ => Err("Invalid arguments supplied to auth".to_string()),
}
}
impl Downloader {
/// Create a new Downloader
pub fn new(tries: usize, user_agent: &str) -> Downloader {
pub fn new(
tries: usize,
user_agent: &str,
cookie: &str,
disable_certs_checks: bool,
auth: &[String],
origin: &Url,
) -> Downloader {
// Create a mapping of hosts to username, password tuples for authentication
let mut auth_map = HashMap::new();
// Iterate over the auth string in chunks of 3 items each for (username, password, host)
for auth_chunk in auth.chunks(AUTH_CHUNK_SIZE) {
// Throwing the error with panic! for now if parsing fails
let (username, password, host) = parse_auth(auth_chunk, origin).unwrap();
auth_map.insert(host, (username, password));
}
let mut headers = HeaderMap::new();
headers.insert(COOKIE, HeaderValue::from_str(cookie).unwrap());
Downloader {
client: reqwest::blocking::ClientBuilder::new()
.default_headers(headers)
.danger_accept_invalid_certs(disable_certs_checks)
.cookie_store(true)
.user_agent(user_agent)
.build()
.unwrap(),
tries,
auth_map,
}
}
@ -39,30 +103,64 @@ impl Downloader {
}
}
/// Load HTTP auth credentials in a username, password tuple based on the host string
fn get_auth(&self, url: &Url) -> Option<&(String, Option<String>)> {
if let Some(host) = url.host_str() {
self.auth_map.get(&host.to_string())
} else {
None
}
}
///Download the content at this url
fn make_request(&self, url: &Url) -> Result<Response, reqwest::Error> {
match self.client.get(url.clone()).send() {
let req = self.client.get(url.clone());
let req = match self.get_auth(url) {
Some((username, password)) => req.basic_auth(username, password.clone()),
None => req,
};
match req.send() {
Ok(mut data) => {
let data_type = match data.headers().get("content-type") {
Some(data_type) => data_type.to_str().unwrap(),
None => "text/html",
};
lazy_static! {
static ref DATA_TYPE_REGEX: Regex =
Regex::new(r#"^.*(\b[a-z]+/[a-z-+\.]+).*$"#).unwrap();
static ref CHARSET_REGEX: Regex =
Regex::new(r#"^.*charset\s*=\s*["']?([^"'\s;]+).*$"#).unwrap();
}
let filename = if !Downloader::is_html(data_type) {
let (data_type, charset): (String, Option<String>) =
match data.headers().get("content-type") {
Some(content_type_header) => {
let content_type = content_type_header.to_str().unwrap();
let data_type_captures =
DATA_TYPE_REGEX.captures_iter(content_type).next();
let data_type = data_type_captures
.map_or(String::from("text/html"), |first| {
first.get(1).unwrap().as_str().to_lowercase()
});
let charset_captures = CHARSET_REGEX.captures_iter(content_type).next();
let charset = charset_captures
.map(|first| first.get(1).unwrap().as_str().to_lowercase());
(data_type, charset)
}
None => (String::from("text/html"), None),
};
let filename = if !Downloader::is_html(&data_type) {
Downloader::get_filename(data.headers())
} else {
None
};
let data = if Downloader::is_html(data_type) {
ResponseData::Html(data.text().unwrap())
let mut raw_data: Vec<u8> = Vec::new();
data.copy_to(&mut raw_data).unwrap();
let response_data = if Downloader::is_html(&data_type) {
ResponseData::Html(raw_data)
} else {
let mut raw_data: Vec<u8> = Vec::new();
data.copy_to(&mut raw_data).unwrap();
ResponseData::Other(raw_data)
};
Ok(Response::new(data, filename))
Ok(Response::new(response_data, filename, charset))
}
Err(e) => {
@ -93,9 +191,39 @@ mod tests {
#[test]
fn test_download_url() {
let url: Url = Url::parse("https://lwn.net").unwrap();
match Downloader::new(1, "suckit").get(&url) {
match Downloader::new(1, "suckit", "", false, &[], &url).get(&url) {
Err(e) => assert!(false, "Fail to download lwn.net: {:?}", e),
_ => {}
}
}
#[test]
fn test_parse_auth() {
assert_eq!(
parse_auth(
&["".to_string(), "pw".to_string()],
&Url::parse("https://example.com/").unwrap(),
),
Err("Invalid arguments supplied to auth".to_string())
);
assert_eq!(
parse_auth(
&["username".to_string()],
&Url::parse("https://example.com/").unwrap(),
),
Ok(("username".to_string(), None, "example.com".to_string()))
);
assert_eq!(
parse_auth(
&[
"un".to_string(),
"pw".to_string(),
"h".to_string(),
"t".to_string()
],
&Url::parse("https://example.com/").unwrap(),
),
Ok(("un".to_string(), Some("pw".to_string()), "h".to_string()))
)
}
}

8
src/lib.rs Normal file
View file

@ -0,0 +1,8 @@
pub mod args;
pub mod disk;
pub mod dom;
pub mod downloader;
pub mod logger;
pub mod response;
pub mod scraper;
pub mod url_helper;

View file

@ -39,7 +39,7 @@ impl Logger {
"ERROR".red(),
message
);
panic!(message)
panic!("{}", message)
}
}

View file

@ -1,18 +0,0 @@
mod args;
mod disk;
mod dom;
mod downloader;
mod logger;
mod response;
mod scraper;
mod url_helper;
use scraper::Scraper;
fn main() {
let args = args::Args::collect();
let mut scraper = Scraper::new(args);
scraper.run();
}

View file

@ -1,6 +1,6 @@
/// Separates HTML responses and other content (PDFs, images...)
pub enum ResponseData {
Html(String),
Html(Vec<u8>),
Other(Vec<u8>),
}
@ -8,11 +8,16 @@ pub enum ResponseData {
pub struct Response {
pub data: ResponseData,
pub filename: Option<String>,
pub charset: Option<String>,
}
impl Response {
///Create a new Response
pub fn new(data: ResponseData, filename: Option<String>) -> Response {
Response { data, filename }
pub fn new(data: ResponseData, filename: Option<String>, charset: Option<String>) -> Response {
Response {
data,
filename,
charset,
}
}
}

View file

@ -1,25 +1,28 @@
use crossbeam::channel::{Receiver, Sender, TryRecvError};
use crossbeam::thread;
use url::Url;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::process;
use std::path::Path;
use std::sync::Mutex;
use std::time;
use crossbeam::channel::{Receiver, Sender, TryRecvError};
use crossbeam::thread;
use encoding_rs::Encoding;
use lazy_static::lazy_static;
use pathdiff;
use rand::Rng;
use regex::Regex;
use url::Url;
use super::downloader;
use crate::{error, info, warn};
use super::args;
use super::disk;
use super::dom;
use super::downloader;
use super::response;
use super::url_helper;
use crate::{error, info};
/// Maximum number of empty recv() from the channel
static MAX_EMPTY_RECEIVES: usize = 10;
@ -27,15 +30,15 @@ static MAX_EMPTY_RECEIVES: usize = 10;
static INFINITE_DEPTH: i32 = -1;
/// Sleep duration on empty recv()
static SLEEP_MILLIS: u64 = 100;
static SLEEP_MILLIS: u64 = 500;
static SLEEP_DURATION: time::Duration = time::Duration::from_millis(SLEEP_MILLIS);
/// Producer and Consumer data structure. Handles the incoming requests and
/// adds more as new URLs are found
pub struct Scraper {
args: args::Args,
transmitter: Sender<(Url, i32)>,
receiver: Receiver<(Url, i32)>,
transmitter: Sender<(Url, i32, i32)>,
receiver: Receiver<(Url, i32, i32)>,
downloader: downloader::Downloader,
visited_urls: Mutex<HashSet<String>>,
path_map: Mutex<HashMap<String, String>>,
@ -46,8 +49,21 @@ impl Scraper {
pub fn new(args: args::Args) -> Scraper {
let (tx, rx) = crossbeam::channel::unbounded();
let mut args = args;
if args.visit_filter_is_download_filter {
args.include_visit = args.include_download.clone();
args.exclude_visit = args.exclude_download.clone();
}
Scraper {
downloader: downloader::Downloader::new(args.tries, &args.user_agent),
downloader: downloader::Downloader::new(
args.tries,
&args.user_agent,
&args.cookie,
args.disable_certs_checks,
&args.auth,
&args.origin,
),
args,
transmitter: tx,
receiver: rx,
@ -69,60 +85,175 @@ impl Scraper {
}
/// Push a new URL into the channel
fn push(transmitter: &Sender<(Url, i32)>, url: Url, depth: i32) {
if let Err(e) = transmitter.send((url, depth)) {
fn push(transmitter: &Sender<(Url, i32, i32)>, url: Url, depth: i32, ext_depth: i32) {
if let Err(e) = transmitter.send((url, depth, ext_depth)) {
error!("Couldn't push to channel ! {}", e);
}
}
/// Fix the URLs contained in the DOM-tree so they point to each other
fn fix_domtree(&self, old_url_str: &mut String, new_url: &Url) {
let path_map = self.path_map.lock().unwrap();
let path = path_map.get(new_url.as_str()).unwrap();
/// Fix the URLs contained in the DOM-tree so they point to each other relatively
fn fix_domtree(&self, dom_url: &mut String, source_path: &str, dest_path: &str) {
let source_path_parent = Path::new(source_path).parent().unwrap().to_str().unwrap(); //Unwrap should be safe, there will alway be at least .../index.html
let diff_path = pathdiff::diff_paths(dest_path, source_path_parent).unwrap();
let relative_path = diff_path.as_path().to_str().unwrap();
let new_url_str = url_helper::encode(path);
old_url_str.clear();
old_url_str.push_str(&new_url_str);
dom_url.clear();
dom_url.push_str(relative_path);
}
///Proces an html file: add new url to the chanel and prepare for offline navigation
/// Find the charset of the webpage. ``data`` is not a String as this might not be utf8.
/// Returned String is lower cased
/// This is a hack and should be check in case of a bug
fn find_charset(data: &[u8], http_charset: Option<String>) -> Option<String> {
lazy_static! {
static ref CHARSET_REGEX: Regex =
Regex::new(r#"<meta.*charset\s*=\s*["']?([^"'\s;>]+).*>"#).unwrap();
}
// We don't know the real charset yet. We hope that the charset is ASCII
// compatible, because Rust String are in UTF-8 (also ASCII compatible).
let data_utf8 = unsafe { String::from_utf8_unchecked(Vec::from(data)) };
let captures = CHARSET_REGEX.captures_iter(&data_utf8).next();
// We use the first one, hopping we are in the <head> of the page... or if nothing is found
// we used the http charset (if any).
captures
.map(|first| first.get(1).unwrap().as_str().to_lowercase())
.or(http_charset)
}
/// Proceed to convert the data in utf8.
fn charset_convert(
data: &[u8],
charset_source: &'static Encoding,
charset_dest: &'static Encoding,
) -> Vec<u8> {
let decode_result = charset_source.decode(data);
let decode_bytes = decode_result.0.borrow();
let encode_result = charset_dest.encode(decode_bytes);
encode_result.0.into_owned()
}
/// Check if the charset require conversion
fn needs_charset_conversion(charset: &str) -> bool {
!matches!(charset, "utf-8")
}
/// Proces an html file: add new url to the chanel and prepare for offline navigation
fn handle_html(
scraper: &Scraper,
transmitter: &Sender<(Url, i32)>,
transmitter: &Sender<(Url, i32, i32)>,
url: &Url,
depth: i32,
data: &str,
ext_depth: i32,
data: &[u8],
http_charset: Option<String>,
) -> Vec<u8> {
let dom = dom::Dom::new(data);
let charset_source_str = match Self::find_charset(data, http_charset) {
Some(s) => s,
None => {
warn!("Charset not found for {}, defaulting to UTF-8", url);
String::from("utf-8")
}
};
let need_charset_conversion = Self::needs_charset_conversion(&charset_source_str);
let charset_source = match encoding_rs::Encoding::for_label(charset_source_str.as_bytes()) {
Some(encoder) => encoder,
None => {
warn!(
"Charset {} not supported for {}, defaulting to UTF-8",
charset_source_str, url
);
encoding_rs::UTF_8
}
};
let charset_utf8 = encoding_rs::UTF_8;
let utf8_data = if need_charset_conversion {
Self::charset_convert(data, charset_source, charset_utf8)
} else {
Vec::from(data)
};
let dom = dom::Dom::new(&String::from_utf8_lossy(&utf8_data));
let source_path = match scraper.path_map.lock().unwrap().get(url.as_str()) {
Some(path) => path.clone(),
None => error!("Url {} was not found in the path map", url.as_str()),
};
dom.find_urls_as_strings()
.into_iter()
.filter(|candidate| Scraper::should_visit(candidate, &url))
.filter(|candidate| Scraper::should_visit(scraper, candidate))
.for_each(|next_url| {
let next_full_url = url.join(&next_url).unwrap();
let path = url_helper::to_path(&next_full_url);
let url_to_parse = Scraper::normalize_url(next_url.clone());
if scraper.map_url_path(&next_full_url, path)
&& (scraper.args.depth == INFINITE_DEPTH || depth < scraper.args.depth)
{
Scraper::push(transmitter, next_full_url.clone(), depth + 1);
let next_full_url = match url.join(url_to_parse.as_str()) {
Ok(url) => url,
Err(e) => {
warn!("Failed to parse url: {} | Error: {}", next_url, e);
return;
},
};
let path = url_helper::to_path(&next_full_url, true);
let path_no_fragments = url_helper::to_path(&next_full_url, false);
// We only add urls without fragments to avoid duplication
if scraper.map_url_path(&next_full_url, path_no_fragments.clone()) {
if !Scraper::is_on_another_domain(next_url, url) {
// If we are determining for a local domain
if scraper.args.depth == INFINITE_DEPTH || depth < scraper.args.depth {
Scraper::push(transmitter, next_full_url, depth + 1, ext_depth);
}
} else {
// If we are determining for an external domain
if scraper.args.ext_depth == INFINITE_DEPTH
|| ext_depth < scraper.args.ext_depth
{
Scraper::push(transmitter, next_full_url, depth, ext_depth + 1);
}
}
}
scraper.fix_domtree(next_url, &next_full_url);
scraper.fix_domtree(next_url, &source_path, &path);
});
dom.serialize().into_bytes()
let utf8_data = dom.serialize().into_bytes();
if need_charset_conversion {
Self::charset_convert(&utf8_data, charset_utf8, charset_source)
} else {
utf8_data
}
}
/// Process a single URL
fn handle_url(scraper: &Scraper, transmitter: &Sender<(Url, i32)>, url: Url, depth: i32) {
fn handle_url(
scraper: &Scraper,
transmitter: &Sender<(Url, i32, i32)>,
url: Url,
depth: i32,
ext_depth: i32,
) {
let download_filter_matches = !scraper.args.exclude_download.is_match(url.as_str())
&& scraper.args.include_download.is_match(url.as_str());
// download the page even if the download filter does not match,
// so its links can be discovered and added to the queue
match scraper.downloader.get(&url) {
Ok(response) => {
let data = match response.data {
response::ResponseData::Html(data) => {
Scraper::handle_html(scraper, transmitter, &url, depth, &data)
}
response::ResponseData::Html(data) => Scraper::handle_html(
scraper,
transmitter,
&url,
depth,
ext_depth,
&data,
response.charset,
),
response::ResponseData::Other(data) => data,
};
@ -131,9 +262,7 @@ impl Scraper {
let path_map = scraper.path_map.lock().unwrap();
let path = path_map.get(url.as_str()).unwrap();
if !scraper.args.exclude.is_match(url.as_str())
&& scraper.args.include.is_match(url.as_str())
{
if !scraper.args.dry_run && download_filter_matches {
match response.filename {
Some(filename) => {
disk::save_file(&filename, &data, &scraper.args.output);
@ -147,9 +276,10 @@ impl Scraper {
}
}
Err(e) => {
println!("Couldn't download a page, {:?}", e);
if !scraper.args.continue_on_error {
process::exit(1);
error!("Couldn't download a page, {:?}", e);
} else {
warn!("Couldn't download a page, {:?}", e);
}
}
}
@ -157,15 +287,22 @@ impl Scraper {
scraper.visited_urls.lock().unwrap().insert(url.to_string());
if scraper.args.verbose {
info!("Visited: {}", url);
if download_filter_matches {
info!("Downloaded: {}", url);
} else {
info!("Visited: {}", url);
}
}
}
/// Run through the channel and complete it
pub fn run(&mut self) {
/* Push the origin URL and depth (0) through the channel */
self.map_url_path(&self.args.origin, url_helper::to_path(&self.args.origin));
Scraper::push(&self.transmitter, self.args.origin.clone(), 0);
self.map_url_path(
&self.args.origin,
url_helper::to_path(&self.args.origin, false),
);
Scraper::push(&self.transmitter, self.args.origin.clone(), 0, 0);
thread::scope(|thread_scope| {
for _ in 0..self.args.jobs {
@ -187,9 +324,9 @@ impl Scraper {
}
TryRecvError::Disconnected => panic!("{}", e),
},
Ok((url, depth)) => {
Ok((url, depth, ext_depth)) => {
counter = 0;
Scraper::handle_url(&self_clone, &tx, url, depth);
Scraper::handle_url(self_clone, &tx, url, depth, ext_depth);
self_clone.sleep(&mut rng);
}
}
@ -210,19 +347,22 @@ impl Scraper {
}
// delay_range+1 because gen_range is exclusive on the upper limit
let rand_delay_secs = rng.gen_range(0, random_range + 1);
let rand_delay_secs = rng.gen_range(0..random_range + 1);
let delay_duration = time::Duration::from_secs(base_delay + rand_delay_secs);
std::thread::sleep(delay_duration);
}
/// If a URL should be visited, or does it belong to another domain
fn should_visit(url: &str, base: &Url) -> bool {
/// If a URL should be visited (ignores `mail:`, `javascript:` and other pseudo-links)
fn should_visit(scraper: &Scraper, url: &str) -> bool {
if scraper.args.exclude_visit.is_match(url) || !scraper.args.include_visit.is_match(url) {
return false;
}
match Url::parse(url) {
/* The given candidate is a valid URL, and not a relative path to
* the next one. Therefore, we have to check if this URL belongs
* to the same domain as our current URL. If the candidate has the
* same domain as our base, then we should visit it */
Ok(not_ok) => not_ok.domain() == base.domain(),
* the next one. Therefore, we have to check if this URL is valid.
* If it is, we should visit it.
*/
Ok(not_ok) => not_ok.has_host() && !not_ok.cannot_be_a_base(),
/* Since we couldn't parse this "URL", then it must be a relative
* path or a malformed URL. If the URL is malformed, then it will
@ -230,14 +370,47 @@ impl Scraper {
Err(_) => true,
}
}
/// Replaces `///` with `//`
/// And `//` with `https://`
/// Without this function, if url is `///<domain>.<extension>/`, the app crashes.
fn normalize_url(url: String) -> String {
if url.starts_with("///") {
return url.replacen("///", "https://", 1);
} else if url.starts_with("//") {
return url.replacen("//", "https://", 1);
}
url
}
/// If the URL leads to another domain
fn is_on_another_domain(url: &str, base: &Url) -> bool {
let real_url = Scraper::normalize_url(String::from(url));
match Url::parse(real_url.as_str()) {
/* The given candidate is a valid URL, and not a relative path to
* the next one. Therefore, we have to check if this URL belongs
* to the same domain as our current URL. If the candidate has the
* same domain as our base, and the depth condition is satisfied,
* then we should visit it, */
Ok(not_ok) => not_ok.domain() != base.domain(),
/* Since we couldn't parse this "URL", then it must be a relative
* path or a malformed URL. If the URL is malformed, then it will
* be handled during the join() call in run() */
Err(_) => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use regex::Regex;
use std::path::PathBuf;
use regex::Regex;
use super::*;
#[test]
fn test_zero_delay_range() {
let args = args::Args {
@ -246,13 +419,21 @@ mod tests {
jobs: 1,
tries: 1,
depth: 5,
ext_depth: 0,
delay: 0,
user_agent: "suckit".to_string(),
random_range: 0,
verbose: true,
include: Regex::new("jpg").unwrap(),
exclude: Regex::new("png").unwrap(),
include_visit: Regex::new(".*").unwrap(),
exclude_visit: Regex::new("^$").unwrap(),
include_download: Regex::new("jpg").unwrap(),
exclude_download: Regex::new("png").unwrap(),
visit_filter_is_download_filter: false,
auth: Vec::new(),
continue_on_error: true,
dry_run: false,
disable_certs_checks: false,
cookie: "".to_string(),
};
let _ = Scraper::new(args);
@ -266,15 +447,47 @@ mod tests {
jobs: 1,
tries: 1,
depth: 5,
ext_depth: 0,
delay: 2,
user_agent: "suckit".to_string(),
random_range: 5,
verbose: true,
include: Regex::new("jpg").unwrap(),
exclude: Regex::new("png").unwrap(),
include_visit: Regex::new(".*").unwrap(),
exclude_visit: Regex::new("^$").unwrap(),
include_download: Regex::new("jpg").unwrap(),
exclude_download: Regex::new("png").unwrap(),
visit_filter_is_download_filter: false,
auth: Vec::new(),
continue_on_error: true,
dry_run: false,
disable_certs_checks: false,
cookie: "".to_string(),
};
let _ = Scraper::new(args);
}
#[test]
fn test_charset_parsing_double_quotes() {
assert_eq!(
Scraper::find_charset(b"<meta charset=\"UTF-8\">", None),
Some("utf-8".to_string())
);
assert_eq!(
Scraper::find_charset(b"<meta charset=\"windows-1252\">", None),
Some("windows-1252".to_string())
);
}
#[test]
fn test_charset_parsing_single_quotes() {
assert_eq!(
Scraper::find_charset(b"<meta charset=\'UTF-8\'>", None),
Some("utf-8".to_string())
);
assert_eq!(
Scraper::find_charset(b"<meta charset=\'windows-1252\'>", None),
Some("windows-1252".to_string())
);
}
}

View file

@ -1,27 +1,61 @@
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use std::path::Path;
use md5;
use std::borrow::Cow;
use url::Url;
///Max file name size supported by the file system
const FILE_NAME_MAX_LENGTH: usize = 255;
///Characters that need to be replaced by encode()
const FRAGMENT: &AsciiSet = &CONTROLS.add(b'?');
///Encode special character with '%' representation
pub fn encode(path: &str) -> String {
utf8_percent_encode(path, FRAGMENT).to_string()
}
/// Convert an Url to the corresponding path
pub fn to_path(url: &Url, with_fragment: bool) -> String {
let url_domain = url.host_str().unwrap();
///Convert an Url to the corresponding path
pub fn to_path(url: &Url) -> String {
let url = url.as_str().split("://").collect::<Vec<&str>>()[1];
let mut url = url.replace('/', "_").replace('.', "_");
if url.len() >= FILE_NAME_MAX_LENGTH {
url = url.split_at(FILE_NAME_MAX_LENGTH).0.to_string(); //Shrink too long file name
let mut url_path_and_query = url.path().to_string();
if let Some(query) = url.query() {
url_path_and_query.push_str("__querystring__");
url_path_and_query.push_str(query);
}
let url = url.trim_end_matches('_'); //Remaining '/'
url.to_string()
let path = Path::new(&url_path_and_query);
let mut filename = path.file_name().map_or(String::from(""), |filename| {
filename.to_str().unwrap().to_string()
});
let mut parent = path
.parent()
.map_or("", |filename| filename.to_str().unwrap())
.to_string();
// Ensure the folder names are not too long
parent = parent
.split('/')
.map(|str| {
if str.len() > FILE_NAME_MAX_LENGTH {
Cow::Owned(format!("{:x}", md5::compute(str)))
} else {
Cow::Borrowed(str)
}
})
.collect::<Vec<Cow<str>>>()
.join("/");
if url_path_and_query.ends_with('/') {
filename = "index.html".to_string();
parent = url_path_and_query.trim_end_matches('/').to_string();
} else if Path::new(&filename).extension().is_none() {
parent = url_path_and_query.trim_end_matches('/').to_string();
filename = "index_no_slash.html".to_string();
}
if filename.len() > FILE_NAME_MAX_LENGTH {
let digest = md5::compute(filename);
filename = format!("{:x}.html", digest);
}
match (url.fragment(), with_fragment) {
(Some(fragment), true) => format!("{}{}/{}#{}", url_domain, parent, filename, fragment),
(_, _) => format!("{}{}/{}", url_domain, parent, filename),
}
}
#[cfg(test)]
@ -29,16 +63,79 @@ mod tests {
use super::*;
#[test]
fn url_to_path() {
let str = super::to_path(&Url::parse("https://lwn.net/Kernel/").unwrap());
fn url_to_path_domain_only() {
let str = super::to_path(&Url::parse("https://lwn.net/").unwrap(), false);
assert_eq!(str, "lwn_net_Kernel");
assert_eq!(str, "lwn.net/index.html");
}
#[test]
fn url_to_path_long() {
let str = super::to_path(&Url::parse("https://e8v0pez1lofdxoxgg5vwrnaqkjuvpowp9wtgc2eknlfpjdwmmfti8fcwyjzfdgys3nrgyqyeqjkulpyg9kfiqajza2bwxkinhhpohyrnnoy2bak374tcaxh1ycpboolmx8so9yq9kbcj5wu5cgymqndeqasdak0nvl0ijka6fkkmhhvt43l73bn38rewicd4h1ff2omhpni752jtqyzsjub5coh8dlnr3i35udmkzhxo4db3is9gnqmf3hl.comtest").unwrap());
fn url_to_path_domain_only_no_slash() {
let str = super::to_path(&Url::parse("https://lwn.net").unwrap(), false);
assert_eq!(str, "e8v0pez1lofdxoxgg5vwrnaqkjuvpowp9wtgc2eknlfpjdwmmfti8fcwyjzfdgys3nrgyqyeqjkulpyg9kfiqajza2bwxkinhhpohyrnnoy2bak374tcaxh1ycpboolmx8so9yq9kbcj5wu5cgymqndeqasdak0nvl0ijka6fkkmhhvt43l73bn38rewicd4h1ff2omhpni752jtqyzsjub5coh8dlnr3i35udmkzhxo4db3is9gnqmf3hl_com");
assert_eq!(str, "lwn.net/index.html");
}
#[test]
fn url_to_path() {
let str = super::to_path(
&Url::parse("https://lwn.net/Kernel/index.html").unwrap(),
false,
);
assert_eq!(str, "lwn.net/Kernel/index.html");
}
#[test]
fn url_to_path_index() {
let str = super::to_path(&Url::parse("https://lwn.net/Kernel/").unwrap(), false);
assert_eq!(str, "lwn.net/Kernel/index.html");
}
#[test]
fn url_to_path_index_no_slash() {
let str = super::to_path(&Url::parse("https://lwn.net/Kernel").unwrap(), false);
assert_eq!(str, "lwn.net/Kernel/index_no_slash.html");
}
#[test]
fn url_to_path_fragment() {
let str = super::to_path(
&Url::parse("https://lwn.net/Kernel/#fragment").unwrap(),
true,
);
assert_eq!(str, "lwn.net/Kernel/index.html#fragment");
}
#[test]
fn url_to_path_no_fragment() {
let str = super::to_path(
&Url::parse("https://lwn.net/Kernel/#fragment").unwrap(),
false,
);
assert_eq!(str, "lwn.net/Kernel/index.html");
}
#[test]
fn url_to_path_to_long_md5() {
let str = super::to_path(&Url::parse("https://lwn.net/Kernel/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.html").unwrap(), false);
assert_eq!(str, "lwn.net/Kernel/5ca82767de71fe8930587e82bb994903.html");
}
#[test]
fn url_to_path_querystrings() {
let str = super::to_path(
&Url::parse(
"https://google.com/foobar/platform-redirect/?next=/configuration/releases/",
)
.unwrap(),
false,
);
assert_eq!(str, "google.com/foobar/platform-redirect/__querystring__next=/configuration/releases/index.html");
}
}

64
tests/auth.rs Normal file
View file

@ -0,0 +1,64 @@
//! Tests for using --auth flags for suckit
mod fixtures;
use std::fs::read_dir;
use std::process::Command;
use std::process::Stdio;
const PAGE: &'static str = "tests/fixtures/";
const IP: &'static str = "0.0.0.0";
// Shouldn't supply credentials to a non-matching host
#[test]
fn auth_different_host() {
let ip = fixtures::spawn_local_http_server(PAGE, true, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[
&url,
"-o",
output_dir,
"-a",
"username password example.com",
])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let paths = read_dir(format!("{}/{}", output_dir, IP)).unwrap();
// Only the initial invalid response file should be present
assert_eq!(paths.count(), 1);
}
// Should authenticate with credentials to host (defaulting to origin host)
#[test]
fn auth_valid() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir, "-a", "username password"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let paths = read_dir(format!("{}/{}", output_dir, IP)).unwrap();
// Should load multiple paths, not just the invalid auth response
assert!(paths.count() > 1);
}

View file

@ -0,0 +1,41 @@
//! Test for charset detection/conversion
mod fixtures;
use std::fs;
use std::process::{Command, Stdio};
use std::sync::Once;
const PAGE: &'static str = "tests/fixtures";
const PAGE_META: &'static str = "tests/fixtures/charset_test_html.html";
const IP: &'static str = "0.0.0.0";
#[test]
fn test_html_charset_found() {
// Spawn a single instance of a local http server usable by all tests in this module.
let addr = fixtures::spawn_local_http_server(PAGE, false, None);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let file_dir = format!("{}/{}", output_dir, IP);
let url = format!("http://{}/charset_test_html.html", addr);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let file_path = fs::read_dir(file_dir)
.unwrap()
.next()
.unwrap()
.unwrap()
.path(); // There is only one file in the directory
let data_source = fs::read(PAGE_META).unwrap();
let data_downloaded = fs::read(file_path).unwrap();
assert!(fixtures::do_vecs_match(&data_source, &data_downloaded));
}

View file

@ -0,0 +1,42 @@
//! Test for charset detection/conversion
mod fixtures;
use std::fs;
use std::process::{Command, Stdio};
const PAGE: &'static str = "tests/fixtures/";
const PAGE_NO_META: &'static str = "tests/fixtures/charset_test_html_no_meta.html";
const IP: &'static str = "0.0.0.0";
#[test]
fn test_html_charset_not_found() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
// Spawn a single instance of a local http server usable by all tests in this module.
let file_dir = format!("{}/{}", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let file_path = fs::read_dir(file_dir)
.unwrap()
.next()
.unwrap()
.unwrap()
.path(); // There is only one file in the directory
let data_source = fs::read(PAGE_NO_META).unwrap();
let data_downloaded = fs::read(file_path).unwrap();
assert!(!fixtures::do_vecs_match(&data_source, &data_downloaded));
}

View file

@ -0,0 +1,48 @@
//! Test for charset detection/conversion
mod fixtures;
use std::fs;
use std::process::{Command, Stdio};
use lazy_static::lazy_static;
const PAGE: &'static str = "tests/fixtures/";
const PAGE_NO_META: &'static str = "tests/fixtures/charset_test_html_no_meta.html";
const IP: &'static str = "0.0.0.0";
lazy_static! {
static ref CHARSET_HEADER: Vec<(&'static str, &'static str)> =
vec![("Content-Type", "charset=windows-1252")];
}
#[test]
fn test_http_charset_found() {
let ip = fixtures::spawn_local_http_server(PAGE, false, Some(&CHARSET_HEADER));
let url = format!("http://{}/charset_test_html_no_meta.html", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let file_dir = format!("{}/{}", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let file_path = fs::read_dir(file_dir)
.unwrap()
.next()
.unwrap()
.unwrap()
.path(); // There is only one file in the directory
let data_source = fs::read(PAGE_NO_META).unwrap();
let data_downloaded = fs::read(file_path).unwrap();
assert!(fixtures::do_vecs_match(&data_source, &data_downloaded));
}

View file

@ -0,0 +1,42 @@
//! Test for charset detection/conversion
mod fixtures;
use std::fs;
use std::process::{Command, Stdio};
const PAGE: &'static str = "tests/fixtures/";
const PAGE_NO_META: &'static str = "tests/fixtures/charset_test_html_no_meta.html";
const IP: &'static str = "0.0.0.0";
#[test]
fn test_http_charset_found() {
// Spawn a single instance of a local http server usable by all tests in this module.
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let file_dir = format!("{}/{}", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let file_path = fs::read_dir(file_dir)
.unwrap()
.next()
.unwrap()
.unwrap()
.path(); // There is only one file in the directory
let data_source = fs::read(PAGE_NO_META).unwrap();
let data_downloaded = fs::read(file_path).unwrap();
assert!(!fixtures::do_vecs_match(&data_source, &data_downloaded));
}

62
tests/external.rs Normal file
View file

@ -0,0 +1,62 @@
//! Tests for using --ext-depth
mod fixtures;
use fixtures::get_file_count_with_pattern;
use std::fs::read_dir;
use std::process::Command;
use std::process::Stdio;
use std::sync::Once;
const PAGE: &'static str = "tests/fixtures/";
const IP: &'static str = "0.0.0.0";
// Test to use include flag for downloading pages only matching the given pattern.
#[test]
fn with_external() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let local = format!("{}/{}/", output_dir, IP);
let external = format!("{}/{}/", output_dir, "google.com");
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir, "-d", "0", "--ext-depth", "1"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let path_local = read_dir(&local).unwrap();
let path_external = read_dir(&external).unwrap();
assert_eq!(path_local.count() + path_external.count(), 2);
}
#[test]
fn without_external() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let external = format!("{}/{}/", output_dir, "google.com");
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir, "-d", "0", "--ext-depth", "0"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let path_external = read_dir(&external);
assert!(path_external.is_err());
}

View file

@ -8,28 +8,55 @@ use std::process::Command;
use std::process::Stdio;
use std::sync::Once;
const SUCKIT: &'static str = "target/debug/suckit";
const ADDR: &'static str = "http://0.0.0.0:8000";
const PAGE: &'static str = "tests/fixtures/";
const IP: &'static str = "0.0.0.0";
static START: Once = Once::new();
#[test]
fn test_include_exclude() {
// Spawn a single instance of a local http server usable by all tests in this module.
START.call_once(|| {
fixtures::spawn_local_http_server();
});
fn visit_filter_is_download_filter() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
// Tests below are grouped together as they depend on the local_http_server above.
include_filter();
include_multiple_filters();
exclude_filter();
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let files_dir = format!("{}/{}/", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[
&url,
"-o",
output_dir,
"-v",
"-e",
"no_download_no_visit.html",
"--visit-filter-is-download-filter",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let result = cmd.wait_with_output().unwrap();
let stdout_str = unsafe { String::from_utf8_unchecked(result.stdout) };
assert!(result.status.success());
let paths = read_dir(&files_dir).unwrap();
assert!(!stdout_str.contains("should_not_get_visited.html"));
}
// Test to use include flag for downloading pages only matching the given pattern.
fn include_filter() {
let output_dir = "w2";
let mut cmd = Command::new(SUCKIT)
.args(&[ADDR, "-o", "w2", "-i", "mp[3-4]", "-j", "16"])
// Test to use include flag for visiting pages only matching the given pattern.
#[test]
fn visit_include_filter() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let files_dir = format!("{}/{}/", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir, "--include-visit", "mp[3-4]"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
@ -37,39 +64,131 @@ fn include_filter() {
let status = cmd.wait().unwrap();
assert!(status.success());
let paths = read_dir(output_dir).unwrap();
let paths = read_dir(&files_dir).unwrap();
assert_eq!(
paths.count() - 1, // minus one because of index.html which is downloaded unconditionally
get_file_count_with_pattern(".mp3", &files_dir).unwrap()
);
}
// Test demonstrating usage of multiple include patterns for visiting pages only matching the given pattern.
#[test]
fn visit_include_multiple_filters() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let files_dir = format!("{}/{}/", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir, "--include-visit", "(mp[3-4])|(txt)"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let paths = read_dir(&files_dir).unwrap();
let mp3_count = get_file_count_with_pattern(".mp3", &files_dir).unwrap();
let txt_count = get_file_count_with_pattern(".txt", &files_dir).unwrap();
assert_eq!(
paths.count() - 1, // minus one because of index.html which is downloaded unconditionally
mp3_count + txt_count
);
}
// Test to use exclude flag for excluding pages matching the given pattern.
#[test]
fn visit_exclude_filter() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let files_dir = format!("{}/{}/", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir, "--exclude-visit", "jpe?g"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let jpeg_count = get_file_count_with_pattern(".jpe?g", &files_dir).unwrap();
assert_eq!(jpeg_count, 0);
}
// Test to use include flag for downloading pages only matching the given pattern.
#[test]
fn download_include_filter() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let files_dir = format!("{}/{}/", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir, "-i", "mp[3-4]"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let paths = read_dir(&files_dir).unwrap();
assert_eq!(
paths.count(),
get_file_count_with_pattern("*_mp3", output_dir).unwrap()
get_file_count_with_pattern(".mp3", &files_dir).unwrap()
);
std::fs::remove_dir_all(output_dir).unwrap();
}
// Test demonstrating usage of multiple include patterns for downloading pages only matching the given pattern.
fn include_multiple_filters() {
let output_dir = "w1";
let mut cmd = Command::new(SUCKIT)
.args(&[ADDR, "-o", output_dir, "-i", "(mp[3-4])|(txt)", "-j", "16"])
#[test]
fn download_include_multiple_filters() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let files_dir = format!("{}/{}/", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir, "-i", "(mp[3-4])|(txt)"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let status = cmd.wait().unwrap();
assert!(status.success());
let paths = read_dir(output_dir).unwrap();
let mp3_count = get_file_count_with_pattern("*_mp3", output_dir).unwrap();
let txt_count = get_file_count_with_pattern("*_txt", output_dir).unwrap();
assert_eq!(paths.count(), mp3_count + txt_count);
std::fs::remove_dir_all(output_dir).unwrap();
let paths = read_dir(&files_dir).unwrap();
let mp3_count = get_file_count_with_pattern(".mp3", &files_dir).unwrap();
let txt_count = get_file_count_with_pattern(".txt", &files_dir).unwrap();
assert_eq!(paths.count(), mp3_count + txt_count);
}
// Test to use exclude flag for excluding pages matching the given pattern.
fn exclude_filter() {
let output_dir = "w3";
let mut cmd = Command::new(SUCKIT)
.args(&[ADDR, "-o", output_dir, "-e", "jpe?g", "-j", "16"])
#[test]
fn download_exclude_filter() {
let ip = fixtures::spawn_local_http_server(PAGE, false, None);
let url = format!("http://{}", ip);
let tempdir = mktemp::Temp::new_dir().unwrap();
let output_dir = tempdir.to_str().unwrap();
let files_dir = format!("{}/{}/", output_dir, IP);
let mut cmd = Command::new(env!("CARGO_BIN_EXE_suckit"))
.args(&[&url, "-o", output_dir, "-e", "jpe?g"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
@ -77,11 +196,7 @@ fn exclude_filter() {
let status = cmd.wait().unwrap();
assert!(status.success());
let paths = read_dir(output_dir).unwrap();
let mp3_count = get_file_count_with_pattern("*_mp3", output_dir).unwrap();
let txt_count = get_file_count_with_pattern("*_txt", output_dir).unwrap();
let index_file = 1;
assert_eq!(paths.count(), mp3_count + txt_count + index_file);
std::fs::remove_dir_all(output_dir).unwrap();
let paths = read_dir(&files_dir).unwrap();
let jpeg_count = get_file_count_with_pattern(".jpe?g", &files_dir).unwrap();
assert_eq!(jpeg_count, 0);
}

13
tests/fixtures/charset_test_html.html vendored Normal file
View file

@ -0,0 +1,13 @@
<!-- This file is encoded in 'windows-1252' charset with no newline at the end.
Be careful when modifying it. To save it correctly on:
- vim: :write ++enc=windows-1252
This file should not have a newline. Don't forget to run ``truncate -s -1``
in case there is one.
--><html><head>
<meta content="text/html; charset=windows-1252" http-equiv="Content-Type">
<title>Gamle Gjerpen - Ei Bygdebok for nett.</title>
</head>
<body>
<p>Gamle Valebø Redigering av sidene pågår.</p>
</body></html>

View file

@ -0,0 +1,12 @@
<!-- This file is encoded in 'windows-1252' charset with no newline at the end.
Be careful when modifying it. To save it correctly on:
- vim: :write ++enc=windows-1252
This file should not have a newline. Don't forget to run ``truncate -s -1``
in case there is one.
--><html><head>
<title>Gamle Gjerpen - Ei Bygdebok for nett.</title>
</head>
<body>
<p>Gamle Valebø Redigering av sidene pågår.</p>
</body></html>

View file

@ -9,5 +9,7 @@
<a href="jpeg.jpg" download>JPG File</a>
<a href="mp3.mp3" download="">MP3 File</a>
<a href="file.txt" download="">Text File</a>
<a href="https://google.com" download="">Google</a>
<a href="no_download_no_visit.html">No download</a>
</body>
</html>

121
tests/fixtures/mod.rs vendored
View file

@ -1,56 +1,95 @@
use std::fs::File;
use std::process::Command;
use std::process::Stdio;
use std::thread;
use tiny_http::{Response, Server};
const PAGE: &'static str = "tests/fixtures/index.html";
use portpicker;
use subprocess::Exec;
use tiny_http::{Header, Response, Server};
pub fn spawn_local_http_server() {
let server = Server::http("0.0.0.0:8000").unwrap();
println!("Spawning http server");
const AUTH_HEADER: &str = "Authorization";
const AUTH_CREDENTIALS: &str = "Basic dXNlcm5hbWU6cGFzc3dvcmQ="; // base64-encoded "username:password"
pub fn spawn_local_http_server(
page: &'static str,
requires_auth: bool,
headers: Option<&'static Vec<(&'static str, &'static str)>>,
) -> String {
let port = portpicker::pick_unused_port().unwrap();
let addr = format!("0.0.0.0:{}", port);
let server = Server::http(&addr).unwrap();
thread::spawn(move || {
for request in server.incoming_requests() {
let response = Response::from_file(File::open(PAGE).unwrap());
// Authenticate request from headers if provided
let auth_header = request
.headers()
.iter()
.find(|h| h.field.equiv(AUTH_HEADER));
let valid_auth = check_auth_credentials(auth_header);
let mut response = if requires_auth && !valid_auth {
let mut response = Response::from_string("Invalid auth").with_status_code(401);
let h = Header::from_bytes("WWW-Authenticate", r#"Basic realm="Test""#).unwrap();
response.add_header(h);
response.boxed()
} else {
let file = match request.url() {
"/" => format!("{}{}", page, "index.html"),
other => format!("{}{}", page, other),
};
// panic!("File = {}", file);
Response::from_file(File::open(file).unwrap()).boxed()
};
match headers {
Some(vec) => {
let mut key_vec: Vec<u8> = vec![];
let mut value_vec: Vec<u8> = vec![];
for (key, value) in vec {
key_vec.extend_from_slice(key.as_bytes());
value_vec.extend_from_slice(value.as_bytes());
}
let h = Header::from_bytes(key_vec, value_vec).unwrap();
response.add_header(h);
}
_ => (),
}
request.respond(response).unwrap();
}
});
return addr;
}
fn check_auth_credentials(auth_header: Option<&Header>) -> bool {
match auth_header {
None => false,
Some(header) => header.value.as_str() == AUTH_CREDENTIALS,
}
}
pub fn get_file_count_with_pattern(pattern: &str, dir: &str) -> Result<usize, ()> {
// Command being run: `ls | grep .mp3 | wc -w`
let mut du_output_child = Command::new("ls")
.args(&[dir])
.stdout(Stdio::piped())
.spawn()
.unwrap();
// Command being run: `ls | grep pattern | wc -w`
if let Some(du_output) = du_output_child.stdout.take() {
let mut sort_output_child = Command::new("egrep")
.arg(pattern)
.stdin(du_output)
.stdout(Stdio::piped())
.spawn()
.unwrap();
du_output_child.wait().unwrap();
if let Some(sort_output) = sort_output_child.stdout.take() {
let head_output_child = Command::new("wc")
.args(&["-w"])
.stdin(sort_output)
.stdout(Stdio::piped())
.spawn()
.unwrap();
let head_stdout = head_output_child.wait_with_output().unwrap();
sort_output_child.wait().unwrap();
return Ok(String::from_utf8(head_stdout.stdout)
.unwrap()
.trim()
.parse()
.unwrap());
}
let cmd = {
// Pipe ('|') is overloaded here and does a real pipe
Exec::shell(format!("ls {}", dir))
| Exec::shell(format!("grep '{}'", pattern))
| Exec::shell("wc -l")
}
.capture();
match cmd {
Ok(capture_data) => {
let stdout = capture_data.stdout_str();
let count = stdout.trim().parse::<usize>().unwrap();
Ok(count)
}
_ => Err(()),
}
Err(())
}
pub fn do_vecs_match<T: PartialEq>(a: &Vec<T>, b: &Vec<T>) -> bool {
let matching = a.iter().zip(b.iter()).filter(|&(a, b)| a == b).count();
matching == a.len() && matching == b.len()
}

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<a href="should_not_get_visited.html">Link</a>
</body>
</html>

View file

33
tests/local_server_setup.sh Executable file
View file

@ -0,0 +1,33 @@
#!/bin/sh
set -e
# Create the local's server directory if necessary
if [[ ! -d local_server ]]; then
mkdir local_server
fi
cd local_server
# Clone repository if necessary
if [[ ! -d linux ]]; then
# Clone a big repository
git clone https://github.com/torvalds/linux
cd linux
# Checkout the 5.9 release
git checkout v5.9
# No need for the .git directory. It creates a ton of files that take too long
# to scrape
rm -rf .git
# Get back to local_server
cd ..
fi
printf "\nWEBSERVER UP\n"
# Start up the local python server
python3 -m http.server

View file

@ -1,16 +1 @@
1, 1, 1
92.01026630401611, 45.85903334617615, 24.938209056854248
92.68794660568237, 45.51665663719177, 36.877896785736084
94.26381759643554, 48.01623530387879, 39.034280157089235
94.51657667160035, 47.43879041671753, 25.296046495437622
94.91624474525452, 47.318924236297605, 38.720614719390866
94.61654992103577, 57.59067335128784, 24.995196104049683
95.28456358909607, 46.86330976486206, 24.708560609817503
77.68792352676391, 39.08626494407654, 19.765260219573975
67.09948434829712, 34.11661868095398, 17.574066400527954
68.2605634689331, 33.728125953674315, 17.433270835876463
70.19220662117004, 41.964057207107544, 17.660782480239867
68.06500535011291, 34.10616636276245, 28.643333148956298
67.92092838287354, 33.87600998878479, 17.644397974014282
67.94186737537385, 33.967066860198976, 17.586638736724854
68.20248901844025, 37.70992383956909, 22.916122150421142

1 1 1 1
92.01026630401611 45.85903334617615 24.938209056854248
92.68794660568237 45.51665663719177 36.877896785736084
94.26381759643554 48.01623530387879 39.034280157089235
94.51657667160035 47.43879041671753 25.296046495437622
94.91624474525452 47.318924236297605 38.720614719390866
94.61654992103577 57.59067335128784 24.995196104049683
95.28456358909607 46.86330976486206 24.708560609817503
77.68792352676391 39.08626494407654 19.765260219573975
67.09948434829712 34.11661868095398 17.574066400527954
68.2605634689331 33.728125953674315 17.433270835876463
70.19220662117004 41.964057207107544 17.660782480239867
68.06500535011291 34.10616636276245 28.643333148956298
67.92092838287354 33.87600998878479 17.644397974014282
67.94186737537385 33.967066860198976 17.586638736724854
68.20248901844025 37.70992383956909 22.916122150421142

View file

@ -1,7 +1,7 @@
#!/usr/bin/python3
# Number of tests for each bench-set
TEST_RETRIES = 10
TEST_RETRIES = 20
# File to store the results
FILENAME = "speed.csv"
@ -9,8 +9,8 @@ FILENAME = "speed.csv"
# Path to the suckit binary
SUCKIT = "suckit"
# URL to download
URL = "http://books.toscrape.com"
# URL to download: localhost
URL = "http://0.0.0.0:8000"
# Path to store the downloaded data
PATH = "/tmp/suckit_speed"
@ -23,6 +23,18 @@ import subprocess
import time
from termcolor import colored
def start_webserver():
print("Launching webserver")
webserver_pid = subprocess.Popen(["./local_server_setup.sh"], stdout = subprocess.PIPE)
while webserver_pid.stdout.readline() != b"WEBSERVER UP\n":
print("Waiting on webserver...", end = "\r")
print("Webserver launched")
return webserver_pid
def parse_args():
global FILENAME
global SUCKIT
@ -120,4 +132,14 @@ def main():
shutil.rmtree(PATH)
if __name__ == "__main__":
main()
webserver_pid = None
try:
webserver_pid = start_webserver()
main()
# Terminate the webserver
webserver_pid.kill()
except: # Kill the webserver if an exception occurs
webserver_pid.kill()