Compare commits

...

44 Commits

Author SHA1 Message Date
dependabot[bot] 9ceb0d4c21
chore(deps): bump docker/setup-qemu-action from 2 to 3 (#283)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 19:27:39 +03:00
Orhun Parmaksız 7afea1721d
chore(docker): enable arm64 builds (#280)
This reverts commit d57ed1125f.
2024-05-11 23:40:06 +03:00
Orhun Parmaksız c04c39b975
docs(readme): remove blog post link 2024-05-05 14:01:07 +03:00
Orhun Parmaksız 32f95ef938
docs(readme): remove public instance link 2024-05-05 13:59:03 +03:00
dependabot[bot] 15e3619479
chore(deps): bump tokio from 1.36.0 to 1.37.0 (#268)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-04-04 00:09:29 +03:00
Orhun Parmaksız 2037530078
refactor(ci): provide codecov token via env
https://github.com/codecov/codecov-action/issues/1292
2024-04-04 00:03:57 +03:00
Orhun Parmaksız 44c07a3eb6
chore(release): prepare for v0.15.0 2024-03-27 23:03:10 +03:00
Orhun Parmaksız 64d783bd0d
chore(deps): bump all dependencies 2024-03-27 22:49:10 +03:00
Orhun Parmaksız 1fd561f869
docs(readme): add packaging status badge 2024-03-27 17:39:35 +03:00
dependabot[bot] c6d6da6296
chore(deps): bump codecov/codecov-action from 3 to 4 (#237)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:35:54 +03:00
Orhun Parmaksız 54e2ddc91a
chore(deps): bump serde from 1.0.196 to 1.0.197 2024-03-27 17:35:12 +03:00
dependabot[bot] 8e505c0da8
chore(deps): bump regex from 1.10.3 to 1.10.4 (#267)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:34:04 +03:00
Orhun Parmaksız 0f0ba72305
chore(deps): bump shuttle dependencies 2024-03-27 17:33:23 +03:00
dependabot[bot] 77e97573ef
chore(deps): bump tokio from 1.35.1 to 1.36.0 (#242)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:27:22 +03:00
dependabot[bot] 40f5d909ca
chore(deps): bump config from 0.13.4 to 0.14.0 (#240)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:27:07 +03:00
dependabot[bot] 567480a21e
chore(deps): bump actix-files from 0.6.2 to 0.6.5 (#225)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:25:43 +03:00
dependabot[bot] e8c342af46
chore(deps): bump ring from 0.17.7 to 0.17.8 (#248)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:25:13 +03:00
dependabot[bot] 1442771a57
chore(deps): bump awc from 3.3.0 to 3.4.0 (#243)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-27 17:24:56 +03:00
Orhun Parmaksız dadd88c240
chore(license): update the copyright years 2024-03-24 23:45:04 +03:00
Orhun Parmaksız e3b00453d9
chore(github): update funding options 2024-03-24 23:44:34 +03:00
Helmut K. C. Tessarek b2acb71d0d
refactor(ci): replace unmaintained action (#266) 2024-03-23 11:47:23 +01:00
Orhun Parmaksız db24d911f6
chore(deps): set up mergify 2024-03-14 23:02:13 +03:00
Helmut K. C. Tessarek 4774de6652
refactor(server): use more specific HTTP status codes (#262)
* refactor(server): use more specific http status codes

* fix: clippy error - oops missed that one

* test(fixtures): add check for status code
2024-03-11 14:00:45 +01:00
Helmut K. C. Tessarek 4987cfe5e5
fix(upload): error on upload with the same filename (#258) 2024-03-08 14:47:13 +01:00
Orhun Parmaksız c60a614258
chore(ci): switch to cargo-llvm-cov for code coverage (#260) 2024-03-08 14:17:42 +01:00
Helmut K. C. Tessarek fa5105deab
test(upload): update the hash of the example file (#254)
changed at source
2024-03-06 05:57:59 +01:00
Jake Howard dae00c42b5
feat(server): do path joins more safely (#247)
* Do path joins more safely

* Improve path cleaning and tests

* Lower-case error message

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>

* Correct handle potential errors in `get_path`

* Use `expect` in tests, rather than `unwrap`

* Correctly handle invalid upload path without panic

* Correctly handle filesystem create errors

* Use result rather than option to allow easier error handling

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-03-06 05:55:59 +01:00
Orhun Parmaksız 12f0e8f3a7
chore(server): gracefully exit when there is no config file found 2024-03-01 22:17:17 +03:00
Helmut K. C. Tessarek db971e6434
feat(server): allow to override filename when using random_url (#233)
* feat(server): allow to override filename when using random_url

* docs(README): remove line from features

* refactor(header): make const private
2024-02-12 13:06:12 +01:00
Helmut K. C. Tessarek 8e6393c6f4
fix(server): return the correct file on multiple files with same name (#234)
* fix(server): file not found, even though on server and not expired

* test: rename test
2024-02-09 21:38:33 +01:00
Helmut K. C. Tessarek e06c18279e
ci(shuttle): fix deployment (#236)
* ci(shuttle): fix deployment

* chore: add dummy file to trigger pipeline

* chore: remove dummy file to trigger pipeline

* ci(shuttle): remove dorny/paths-filter
2024-02-06 18:31:46 +01:00
Helmut K. C. Tessarek 48a3626c7d
fix(server): improve logging for deleted file (#235) 2024-01-31 21:25:04 +01:00
dependabot[bot] 8de2450931
chore(deps): bump serde from 1.0.195 to 1.0.196 (#232)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.195 to 1.0.196.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.195...v1.0.196)

---
updated-dependencies:
- dependency-name: serde
  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>
2024-01-29 15:04:10 +01:00
dependabot[bot] 2003abe8bd
chore(deps): bump dorny/paths-filter from 2 to 3 (#231)
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 2 to 3.
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dorny/paths-filter/compare/v2...v3)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-25 16:57:35 +01:00
dependabot[bot] 4703b26194
chore(deps): bump byte-unit from 5.1.3 to 5.1.4 (#229)
Bumps [byte-unit](https://github.com/magiclen/byte-unit) from 5.1.3 to 5.1.4.
- [Commits](https://github.com/magiclen/byte-unit/compare/v5.1.3...v5.1.4)

---
updated-dependencies:
- dependency-name: byte-unit
  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>
2024-01-24 13:48:07 +01:00
dependabot[bot] e383b6f426
chore(deps): bump regex from 1.10.2 to 1.10.3 (#227)
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.2 to 1.10.3.
- [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.10.2...1.10.3)

---
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>
2024-01-22 16:11:30 +01:00
dependabot[bot] fa7304a5d0
chore(deps): bump actions/cache from 3 to 4 (#226)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 16:59:37 +01:00
dependabot[bot] 90c0477edb
chore(deps): bump serde from 1.0.194 to 1.0.195 (#221)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.194 to 1.0.195.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.194...v1.0.195)

---
updated-dependencies:
- dependency-name: serde
  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>
2024-01-08 12:35:54 +01:00
dependabot[bot] 7f8428c75d
chore(deps): bump byte-unit from 5.1.2 to 5.1.3 (#220)
Bumps [byte-unit](https://github.com/magiclen/byte-unit) from 5.1.2 to 5.1.3.
- [Commits](https://github.com/magiclen/byte-unit/compare/v5.1.2...v5.1.3)

---
updated-dependencies:
- dependency-name: byte-unit
  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>
2024-01-08 12:35:35 +01:00
dependabot[bot] 25a013266c
chore(deps): bump serde from 1.0.193 to 1.0.194 (#219)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.193 to 1.0.194.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.193...v1.0.194)

---
updated-dependencies:
- dependency-name: serde
  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>
2024-01-02 12:41:04 +01:00
Orhun Parmaksız 274bbd3307
fix(lints): apply clippy suggestions 2023-12-31 14:06:46 +03:00
dependabot[bot] c9ba166865
chore(deps): bump awc from 3.2.0 to 3.3.0 (#215)
Bumps [awc](https://github.com/actix/actix-web) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/actix/actix-web/releases)
- [Changelog](https://github.com/actix/actix-web/blob/master/CHANGES.md)
- [Commits](https://github.com/actix/actix-web/compare/awc-v3.2.0...awc-v3.3.0)

---
updated-dependencies:
- dependency-name: awc
  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>
2023-12-25 15:23:17 +01:00
dependabot[bot] 4af7970f48
chore(deps): bump futures-util from 0.3.29 to 0.3.30 (#214)
Bumps [futures-util](https://github.com/rust-lang/futures-rs) from 0.3.29 to 0.3.30.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.29...0.3.30)

---
updated-dependencies:
- dependency-name: futures-util
  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>
2023-12-25 15:18:58 +01:00
dependabot[bot] 87a0ba21ed
chore(deps): bump actix-web from 4.4.0 to 4.4.1 (#213)
Bumps [actix-web](https://github.com/actix/actix-web) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/actix/actix-web/releases)
- [Changelog](https://github.com/actix/actix-web/blob/master/CHANGES.md)
- [Commits](https://github.com/actix/actix-web/compare/web-v4.4.0...web-v4.4.1)

---
updated-dependencies:
- dependency-name: actix-web
  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>
2023-12-25 15:18:34 +01:00
23 changed files with 907 additions and 790 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
github: orhun
patreon: orhunp
custom: ["https://www.buymeacoffee.com/orhun"]
buy_me_a_coffee: orhun

7
.github/mergify.yml vendored Normal file
View File

@ -0,0 +1,7 @@
pull_request_rules:
- name: Automatic merge for Dependabot pull requests
conditions:
- author=dependabot[bot]
actions:
merge:
method: squash

View File

@ -7,17 +7,11 @@ on:
jobs:
audit:
name: Audit
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Run cargo-audit
uses: actions-rs/audit-check@v1
uses: rustsec/audit-check@v1.4.1
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -30,19 +30,22 @@ jobs:
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Setup cargo-tarpaulin
uses: taiki-e/install-action@cargo-tarpaulin
- name: Run tests
run: cargo tarpaulin --out xml --verbose -- --test-threads 1
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate code coverage
run: cargo llvm-cov --lcov --output-path lcov.info -- --test-threads 1
env:
OUT_DIR: target
- name: Upload reports to codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
name: code-coverage-report
file: cobertura.xml
file: lcov.info
flags: unit-tests
fail_ci_if_error: true
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
fixtures:
strategy:

View File

@ -35,12 +35,17 @@ jobs:
type=raw,value=latest
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
@ -68,7 +73,7 @@ jobs:
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}

View File

@ -6,9 +6,6 @@ on:
- master
tags:
- "v*.*.*"
pull_request:
branches:
- master
workflow_dispatch:
jobs:
@ -24,12 +21,6 @@ jobs:
toolchain: stable
profile: minimal
override: true
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
manifest:
- 'Cargo.**'
- name: Install cargo-binstall
uses: taiki-e/install-action@cargo-binstall
- name: Install cargo-shuttle
@ -40,11 +31,7 @@ jobs:
- name: Build
run: cargo build --locked --verbose
- name: Deploy
if: |
${{ startsWith(github.event.ref, 'refs/tags/v') ||
github.event_name == 'workflow_dispatch' ||
steps.changes.outputs.manifest == 'true'
}}
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
run: |
cargo shuttle login --api-key ${{ secrets.SHUTTLE_TOKEN }}
cargo shuttle project restart

View File

@ -5,6 +5,59 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.15.0] - 2024-03-27
### Added
- Allow to override filename when using `random_url` by @tessus in [#233](https://github.com/orhun/rustypaste/pull/233)
Now you can use the `filename` header to override the name of the uploaded file.
For example:
```sh
curl -F "file=@x.txt" -H "filename:override.txt" http://localhost:8000
```
Even if `random_url` is set, the filename will be override.txt
[`rustypaste-cli`](https://github.com/orhun/rustypaste-cli) also has a new argument for overriding the file name:
```sh
rpaste -n filename-on-server.txt awesome.txt
```
- Use more specific HTTP status codes by @tessus in [#262](https://github.com/orhun/rustypaste/pull/262)
`rustypaste` now returns more appropriate status codes in the following 2 cases (instead of a generic 500 code):
- If the mime type is on the blacklist: `UnsupportedMediaType` (415)
- If the file already exists: `Conflict` (409)
### Changed
- Do path joins more safely by @RealOrangeOne in [#247](https://github.com/orhun/rustypaste/pull/247)
- Gracefully exit when there is no config file found by @orhun
- Switch to cargo-llvm-cov for code coverage by @orhun in [#260](https://github.com/orhun/rustypaste/pull/260)
- Replace unmaintained action by @tessus in [#266](https://github.com/orhun/rustypaste/pull/266)
- Set up mergify by @orhun
- Apply clippy suggestions by @orhun
- Update funding options by @orhun
- Update the copyright years by @orhun
- Bump dependencies
### Fixed
- Improve logging for deleted file by @tessus in [#235](https://github.com/orhun/rustypaste/pull/235)
- Fix deployment by @tessus in [#236](https://github.com/orhun/rustypaste/pull/236)
- Return the correct file on multiple files with same name by @tessus in [#234](https://github.com/orhun/rustypaste/pull/234)
- Update the hash of the example file by @tessus in [#254](https://github.com/orhun/rustypaste/pull/254)
- Error on upload with the same filename by @tessus in [#258](https://github.com/orhun/rustypaste/pull/258)
### New Contributors
- @RealOrangeOne made their first contribution in [#247](https://github.com/orhun/rustypaste/pull/247)
## [0.14.4] - 2023-12-20
### Removed

1129
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "rustypaste"
version = "0.14.4"
version = "0.15.0"
edition = "2021"
description = "A minimal file upload/pastebin service"
authors = ["Orhun Parmaksız <orhunparmaksiz@gmail.com>"]
@ -19,15 +19,15 @@ rustls = ["actix-web/rustls-0_21", "awc/rustls-0_21"]
shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime", "dep:tokio"]
[dependencies]
actix-web = { version = "4.4.0" }
actix-web = { version = "4.5.1" }
actix-web-grants = { version = "4.0.3" }
actix-multipart = "0.6.1"
actix-files = "0.6.2"
shuttle-actix-web = { version = "0.35.1", optional = true }
shuttle-runtime = { version = "0.35.1", optional = true }
awc = { version = "3.2.0" }
serde = "1.0.193"
futures-util = "0.3.29"
actix-files = "0.6.5"
shuttle-actix-web = { version = "0.42.0", optional = true }
shuttle-runtime = { version = "0.42.0", optional = true }
awc = { version = "3.4.0" }
serde = "1.0.197"
futures-util = "0.3.30"
petname = { version = "1.1.3", default-features = false, features = [
"std_rng",
"default_dictionary",
@ -36,26 +36,27 @@ rand = "0.8.5"
dotenvy = "0.15.7"
url = "2.5.0"
mime = "0.3.17"
regex = "1.10.2"
regex = "1.10.4"
serde_regex = "1.1.0"
lazy-regex = "3.1.0"
humantime = "2.1.0"
humantime-serde = "1.1.1"
glob = "0.3.1"
ring = "0.17.7"
ring = "0.17.8"
hotwatch = "0.5.0"
tokio = { version = "1.35.1", optional = true }
tokio = { version = "1.37.0", optional = true }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
uts2ts = "0.4.1"
path-clean = "1.0.1"
[dependencies.config]
version = "0.13.4"
version = "0.14.0"
default-features = false
features = ["toml", "yaml"]
[dependencies.byte-unit]
version = "5.1.2"
version = "5.1.4"
features = ["serde"]
[dependencies.infer]

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2021-2023 Orhun Parmaksız
Copyright (c) 2021-2024 Orhun Parmaksız
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -20,10 +20,6 @@ $ curl https://paste.site.com/safe-toad.txt
some text
```
The public instance is available at [https://rustypaste.shuttleapp.rs](https://rustypaste.shuttleapp.rs) 🚀
Here you can read the blog post about how it is deployed on Shuttle: [https://blog.orhun.dev/blazingly-fast-file-sharing](https://blog.orhun.dev/blazingly-fast-file-sharing)
</details>
<details>
@ -52,6 +48,7 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
- [Paste file from remote URL](#paste-file-from-remote-url)
- [Cleaning up expired files](#cleaning-up-expired-files)
- [Delete file from server](#delete-file-from-server)
- [Override the filename when using `random_url`](#override-the-filename-when-using-random_url)
- [Server](#server)
- [List endpoint](#list-endpoint)
- [HTML Form](#html-form)
@ -98,6 +95,13 @@ Here you can read the blog post about how it is deployed on Shuttle: [https://bl
## Installation
<details>
<summary>Packaging status</summary>
[![Packaging status](https://repology.org/badge/vertical-allrepos/rustypaste.svg)](https://repology.org/project/rustypaste/versions)
</details>
### From crates.io
```sh
@ -254,6 +258,14 @@ $ curl -H "Authorization: <auth_token>" -X DELETE "<server_address>/file.txt"
> The `DELETE` endpoint will not be exposed and will return `404` error if `delete_tokens` are not set.
#### Override the filename when using `random_url`
The generation of a random filename can be overridden by sending a header called `filename`:
```sh
curl -F "file=@x.txt" -H "filename: <file_name>" "<server_address>"
```
### Server
To start the server:

View File

@ -0,0 +1,9 @@
[server]
address = "127.0.0.1:8000"
max_content_length = "10MB"
upload_path = "./upload"
[paste]
random_url = { type = "alphanumeric", length = "4", suffix_mode = true }
default_extension = "txt"
duplicate_files = true

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
content="test data"
setup() {
echo "$content" > file
}
run_test() {
file_url=$(curl -s -F "file=@file" -H "filename:fn_from_header.txt" localhost:8000)
test "$file_url" = "http://localhost:8000/fn_from_header.txt"
test "$content" = "$(cat upload/fn_from_header.txt)"
test "$content" = "$(curl -s $file_url)"
}
teardown() {
rm file
rm -r upload
}

View File

@ -0,0 +1,9 @@
[server]
address = "127.0.0.1:8000"
max_content_length = "10MB"
upload_path = "./upload"
[paste]
random_url = { type = "alphanumeric", length = "4", suffix_mode = true }
default_extension = "txt"
duplicate_files = true

View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
content="test data"
setup() {
echo "$content" > file
}
run_test() {
file_url=$(curl -s -F "file=@file" -H "filename:fn_from_header.txt" localhost:8000)
test "$file_url" = "http://localhost:8000/fn_from_header.txt"
test "$content" = "$(cat upload/fn_from_header.txt)"
test "$content" = "$(curl -s $file_url)"
file_url=$(curl -s -F "file=@file" -H "filename:fn_from_header.txt" localhost:8000)
test "$file_url" = "file already exists"
status_code=$(curl -s -F "file=@file" -H "filename:fn_from_header.txt" -w "%{response_code}" -o /dev/null localhost:8000)
test "$status_code" = "409"
}
teardown() {
rm file
rm -r upload
}

View File

@ -0,0 +1,9 @@
[server]
address = "127.0.0.1:8000"
max_content_length = "10MB"
upload_path = "./upload"
[paste]
default_extension = "txt"
duplicate_files = true
delete_expired_files = { enabled = true, interval = "1h" }

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
content="test data"
setup() {
echo "$content" > file
}
run_test() {
file_url=$(curl -s -F "file=@file" -H "expire:2s" localhost:8000)
file_url=$(curl -s -F "file=@file" -H "expire:1s" localhost:8000)
sleep 2
file_url=$(curl -s -F "file=@file" -H "expire:1m" localhost:8000)
test "$content" = "$(curl -s $file_url)"
}
teardown() {
rm file
rm -r upload
}

View File

@ -11,6 +11,7 @@ setup() {
run_test() {
test "this file type is not permitted" = "$(curl -s -F "file=@file.html" localhost:8000)"
test "this file type is not permitted" = "$(curl -s -F "file=@file.xml" localhost:8000)"
test "415" = "$(curl -s -F "file=@file.xml" -w "%{response_code}" -o /dev/null localhost:8000)"
file_url=$(curl -s -F "file=@file.txt" localhost:8000)
test "$content" = "$(curl -s $file_url)"
}

View File

@ -7,6 +7,9 @@ use std::time::Duration;
/// Custom HTTP header for expiry dates.
pub const EXPIRE: &str = "expire";
/// Custom HTTP header to override filename.
const FILENAME: &str = "filename";
/// Parses the expiry date from the [`custom HTTP header`](EXPIRE).
pub fn parse_expiry_date(headers: &HeaderMap, time: Duration) -> Result<Option<u128>, ActixError> {
if let Some(expire_time) = headers.get(EXPIRE).and_then(|v| v.to_str().ok()) {
@ -18,6 +21,15 @@ pub fn parse_expiry_date(headers: &HeaderMap, time: Duration) -> Result<Option<u
}
}
/// Parses the filename from the header.
pub fn parse_header_filename(headers: &HeaderMap) -> Result<Option<String>, ActixError> {
if let Some(file_name) = headers.get(FILENAME).and_then(|v| v.to_str().ok()) {
Ok(Some(file_name.to_string()))
} else {
Ok(None)
}
}
/// Wrapper for Actix content disposition header.
///
/// Aims to parse the file data from multipart body.

View File

@ -61,6 +61,13 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
}
None => config_folder.join("config.toml"),
};
if !config_path.exists() {
error!(
"{} is not found, please provide a configuration file.",
config_path.display()
);
std::process::exit(1);
}
let config = Config::parse(&config_path).expect("failed to parse config");
trace!("{:#?}", config);
config.warn_deprecation();
@ -71,7 +78,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
// Create necessary directories.
fs::create_dir_all(&server_config.upload_path)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
fs::create_dir_all(paste_type.get_path(&server_config.upload_path))?;
fs::create_dir_all(paste_type.get_path(&server_config.upload_path)?)?;
}
// Set up a watcher for the configuration file changes.
@ -89,7 +96,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, ServerConfig,
let cloned_config = Data::clone(&config);
let config_watcher = move |event: Event| {
if let (EventKind::Modify(ModifyKind::Data(_)), Some(path)) =
(event.kind, event.paths.get(0))
(event.kind, event.paths.first())
{
match Config::parse(path) {
Ok(config) => match cloned_config.write() {

View File

@ -58,12 +58,12 @@ impl PasteType {
}
/// Returns the given path with [`directory`](Self::get_dir) adjoined.
pub fn get_path(&self, path: &Path) -> PathBuf {
pub fn get_path(&self, path: &Path) -> IoResult<PathBuf> {
let dir = self.get_dir();
if dir.is_empty() {
path.to_path_buf()
Ok(path.to_path_buf())
} else {
path.join(dir)
util::safe_path_join(path, Path::new(&dir))
}
}
@ -88,6 +88,7 @@ impl Paste {
/// - If `file_name` does not have an extension, it is replaced with [`default_extension`].
/// - If `file_name` is "-", it is replaced with "stdin".
/// - If [`random_url.enabled`] is `true`, `file_name` is replaced with a pet name or random string.
/// - If `header_filename` is set, it will override the filename.
///
/// [`default_extension`]: crate::config::PasteConfig::default_extension
/// [`random_url.enabled`]: crate::random::RandomURLConfig::enabled
@ -95,15 +96,15 @@ impl Paste {
&self,
file_name: &str,
expiry_date: Option<u128>,
header_filename: Option<String>,
config: &Config,
) -> IoResult<String> {
) -> Result<String, Error> {
let file_type = infer::get(&self.data);
if let Some(file_type) = file_type {
for mime_type in &config.paste.mime_blacklist {
if mime_type == file_type.mime_type() {
return Err(IoError::new(
IoErrorKind::Other,
String::from("this file type is not permitted"),
return Err(error::ErrorUnsupportedMediaType(
"this file type is not permitted",
));
}
}
@ -120,10 +121,9 @@ impl Paste {
if let Some(handle_spaces_config) = config.server.handle_spaces {
file_name = handle_spaces_config.process_filename(&file_name);
}
let mut path = self
.type_
.get_path(&config.server.upload_path)
.join(&file_name);
let mut path =
util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
let mut parts: Vec<&str> = file_name.split('.').collect();
let mut dotfile = false;
let mut lower_bound = 1;
@ -166,11 +166,20 @@ impl Paste {
}
path.set_file_name(file_name);
path.set_extension(extension);
if let Some(header_filename) = header_filename {
file_name = header_filename;
path.set_file_name(file_name);
}
let file_name = path
.file_name()
.map(|v| v.to_string_lossy())
.unwrap_or_default()
.to_string();
let file_path = util::glob_match_file(path.clone())
.map_err(|_| IoError::new(IoErrorKind::Other, String::from("path is not valid")))?;
if file_path.is_file() && file_path.exists() {
return Err(error::ErrorConflict("file already exists\n"));
}
if let Some(timestamp) = expiry_date {
path.set_file_name(format!("{file_name}.{timestamp}"));
}
@ -235,7 +244,7 @@ impl Paste {
.to_string());
}
}
Ok(self.store_file(file_name, expiry_date, &config)?)
self.store_file(file_name, expiry_date, None, &config)
}
/// Writes an URL to a file in upload directory.
@ -255,10 +264,8 @@ impl Paste {
file_name = random_text;
}
}
let mut path = self
.type_
.get_path(&config.server.upload_path)
.join(&file_name);
let mut path =
util::safe_path_join(self.type_.get_path(&config.server.upload_path)?, &file_name)?;
if let Some(timestamp) = expiry_date {
path.set_file_name(format!("{file_name}.{timestamp}"));
}
@ -295,7 +302,7 @@ mod tests {
data: vec![65, 66, 67],
type_: PasteType::File,
};
let file_name = paste.store_file("test.txt", None, &config)?;
let file_name = paste.store_file("test.txt", None, None, &config)?;
assert_eq!("ABC", fs::read_to_string(&file_name)?);
assert_eq!(
Some("txt"),
@ -315,7 +322,7 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert!(file_name.ends_with(".tar.gz"));
assert!(file_name.starts_with("foo."));
@ -331,7 +338,7 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file(".foo.tar.gz", None, &config)?;
let file_name = paste.store_file(".foo.tar.gz", None, None, &config)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert!(file_name.ends_with(".tar.gz"));
assert!(file_name.starts_with(".foo."));
@ -347,7 +354,7 @@ mod tests {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file("foo.tar.gz", None, &config)?;
let file_name = paste.store_file("foo.tar.gz", None, None, &config)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert!(file_name.ends_with(".tar.gz"));
fs::remove_file(file_name)?;
@ -358,7 +365,7 @@ mod tests {
data: vec![120, 121, 122],
type_: PasteType::File,
};
let file_name = paste.store_file(".foo", None, &config)?;
let file_name = paste.store_file(".foo", None, None, &config)?;
assert_eq!("xyz", fs::read_to_string(&file_name)?);
assert_eq!(".foo.txt", file_name);
fs::remove_file(file_name)?;
@ -373,7 +380,7 @@ mod tests {
data: vec![120, 121, 122],
type_: PasteType::File,
};
let file_name = paste.store_file("random", None, &config)?;
let file_name = paste.store_file("random", None, None, &config)?;
assert_eq!("xyz", fs::read_to_string(&file_name)?);
assert_eq!(
Some("bin"),
@ -383,8 +390,52 @@ mod tests {
);
fs::remove_file(file_name)?;
config.paste.random_url = Some(RandomURLConfig {
length: Some(4),
type_: RandomURLType::Alphanumeric,
suffix_mode: Some(true),
..RandomURLConfig::default()
});
let paste = Paste {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file(
"filename.txt",
None,
Some("fn_from_header.txt".to_string()),
&config,
)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert_eq!("fn_from_header.txt", file_name);
fs::remove_file(file_name)?;
config.paste.random_url = Some(RandomURLConfig {
length: Some(4),
type_: RandomURLType::Alphanumeric,
suffix_mode: Some(true),
..RandomURLConfig::default()
});
let paste = Paste {
data: vec![116, 101, 115, 115, 117, 115],
type_: PasteType::File,
};
let file_name = paste.store_file(
"filename.txt",
None,
Some("fn_from_header".to_string()),
&config,
)?;
assert_eq!("tessus", fs::read_to_string(&file_name)?);
assert_eq!("fn_from_header", file_name);
fs::remove_file(file_name)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
fs::create_dir_all(paste_type.get_path(&config.server.upload_path))?;
fs::create_dir_all(
paste_type
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)?;
}
config.paste.random_url = None;
@ -393,9 +444,10 @@ mod tests {
type_: PasteType::Oneshot,
};
let expiry_date = util::get_system_time()?.as_millis() + 100;
let file_name = paste.store_file("test.file", Some(expiry_date), &config)?;
let file_name = paste.store_file("test.file", Some(expiry_date), None, &config)?;
let file_path = PasteType::Oneshot
.get_path(&config.server.upload_path)
.expect("Bad upload path")
.join(format!("{file_name}.{expiry_date}"));
assert_eq!("test", fs::read_to_string(&file_path)?);
fs::remove_file(file_path)?;
@ -412,6 +464,7 @@ mod tests {
let file_name = paste.store_url(None, &config)?;
let file_path = PasteType::Url
.get_path(&config.server.upload_path)
.expect("Bad upload path")
.join(&file_name);
assert_eq!(url, fs::read_to_string(&file_path)?);
fs::remove_file(file_path)?;
@ -439,15 +492,20 @@ mod tests {
.await?;
let file_path = PasteType::RemoteFile
.get_path(&config.server.upload_path)
.expect("Bad upload path")
.join(file_name);
assert_eq!(
"8c712905b799905357b8202d0cb7a244cefeeccf7aa5eb79896645ac50158ffa",
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
util::sha256_digest(&*paste.data)?
);
fs::remove_file(file_path)?;
for paste_type in &[PasteType::Url, PasteType::Oneshot] {
fs::remove_dir(paste_type.get_path(&config.server.upload_path))?;
fs::remove_dir(
paste_type
.get_path(&config.server.upload_path)
.expect("Bad upload path"),
)?;
}
Ok(())

View File

@ -4,7 +4,7 @@ use crate::file::Directory;
use crate::header::{self, ContentDisposition};
use crate::mime as mime_util;
use crate::paste::{Paste, PasteType};
use crate::util;
use crate::util::{self, safe_path_join};
use actix_files::NamedFile;
use actix_multipart::Multipart;
use actix_web::http::StatusCode;
@ -89,12 +89,11 @@ async fn serve(
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let path = config.server.upload_path.join(&*file);
let mut path = util::glob_match_file(path)?;
let mut path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
let mut paste_type = PasteType::File;
if !path.exists() || path.is_dir() {
for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
let alt_path = type_.get_path(&config.server.upload_path).join(&*file);
let alt_path = safe_path_join(type_.get_path(&config.server.upload_path)?, &*file)?;
let alt_path = util::glob_match_file(alt_path)?;
if alt_path.exists()
|| path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir())
@ -159,13 +158,12 @@ async fn delete(
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
let path = config.server.upload_path.join(&*file);
let path = util::glob_match_file(path)?;
let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
if !path.is_file() || !path.exists() {
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
}
match fs::remove_file(path) {
Ok(_) => info!("deleted file: {:?}", file),
Ok(_) => info!("deleted file: {:?}", file.to_string()),
Err(e) => {
error!("cannot delete file: {}", e);
return Err(error::ErrorInternalServerError("cannot delete file"));
@ -225,6 +223,7 @@ async fn upload(
}
let mut urls: Vec<String> = Vec::new();
while let Some(item) = payload.next().await {
let header_filename = header::parse_header_filename(request.headers())?;
let mut field = item?;
let content = ContentDisposition::from(field.content_disposition().clone());
if let Ok(paste_type) = PasteType::try_from(&content) {
@ -274,7 +273,12 @@ async fn upload(
let config = config
.read()
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
paste.store_file(content.get_file_name()?, expiry_date, &config)?
paste.store_file(
content.get_file_name()?,
expiry_date,
header_filename,
&config,
)?
}
PasteType::RemoteFile => {
paste
@ -853,6 +857,108 @@ mod tests {
Ok(())
}
#[actix_web::test]
async fn test_upload_file_override_filename() -> Result<(), Error> {
let mut config = Config::default();
config.server.upload_path = env::current_dir()?;
let app = test::init_service(
App::new()
.app_data(Data::new(RwLock::new(config)))
.app_data(Data::new(Client::default()))
.configure(configure_routes),
)
.await;
let file_name = "test_file.txt";
let header_filename = "fn_from_header.txt";
let timestamp = util::get_system_time()?.as_secs().to_string();
let response = test::call_service(
&app,
get_multipart_request(&timestamp, "file", file_name)
.insert_header((
header::HeaderName::from_static("filename"),
header::HeaderValue::from_static("fn_from_header.txt"),
))
.to_request(),
)
.await;
assert_eq!(StatusCode::OK, response.status());
assert_body(
response.into_body(),
&format!("http://localhost:8080/{header_filename}\n"),
)
.await?;
let serve_request = TestRequest::get()
.uri(&format!("/{header_filename}"))
.to_request();
let response = test::call_service(&app, serve_request).await;
assert_eq!(StatusCode::OK, response.status());
assert_body(response.into_body(), &timestamp).await?;
fs::remove_file(header_filename)?;
let serve_request = TestRequest::get()
.uri(&format!("/{header_filename}"))
.to_request();
let response = test::call_service(&app, serve_request).await;
assert_eq!(StatusCode::NOT_FOUND, response.status());
Ok(())
}
#[actix_web::test]
async fn test_upload_same_filename() -> Result<(), Error> {
let mut config = Config::default();
config.server.upload_path = env::current_dir()?;
let app = test::init_service(
App::new()
.app_data(Data::new(RwLock::new(config)))
.app_data(Data::new(Client::default()))
.configure(configure_routes),
)
.await;
let file_name = "test_file.txt";
let header_filename = "fn_from_header.txt";
let timestamp = util::get_system_time()?.as_secs().to_string();
let response = test::call_service(
&app,
get_multipart_request(&timestamp, "file", file_name)
.insert_header((
header::HeaderName::from_static("filename"),
header::HeaderValue::from_static("fn_from_header.txt"),
))
.to_request(),
)
.await;
assert_eq!(StatusCode::OK, response.status());
assert_body(
response.into_body(),
&format!("http://localhost:8080/{header_filename}\n"),
)
.await?;
let timestamp = util::get_system_time()?.as_secs().to_string();
let response = test::call_service(
&app,
get_multipart_request(&timestamp, "file", file_name)
.insert_header((
header::HeaderName::from_static("filename"),
header::HeaderValue::from_static("fn_from_header.txt"),
))
.to_request(),
)
.await;
assert_eq!(StatusCode::CONFLICT, response.status());
assert_body(response.into_body(), "file already exists\n").await?;
fs::remove_file(header_filename)?;
Ok(())
}
#[actix_web::test]
#[allow(deprecated)]
async fn test_upload_duplicate_file() -> Result<(), Error> {
@ -1003,7 +1109,7 @@ mod tests {
let body = response.into_body();
let body_bytes = actix_web::body::to_bytes(body).await?;
assert_eq!(
"8c712905b799905357b8202d0cb7a244cefeeccf7aa5eb79896645ac50158ffa",
"70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed",
util::sha256_digest(&*body_bytes)?
);
@ -1031,7 +1137,9 @@ mod tests {
)
.await;
let url_upload_path = PasteType::Url.get_path(&config.server.upload_path);
let url_upload_path = PasteType::Url
.get_path(&config.server.upload_path)
.expect("Bad upload path");
fs::create_dir_all(&url_upload_path)?;
let response = test::call_service(
@ -1069,7 +1177,9 @@ mod tests {
)
.await;
let oneshot_upload_path = PasteType::Oneshot.get_path(&config.server.upload_path);
let oneshot_upload_path = PasteType::Oneshot
.get_path(&config.server.upload_path)
.expect("Bad upload path");
fs::create_dir_all(&oneshot_upload_path)?;
let file_name = "oneshot.txt";
@ -1129,7 +1239,9 @@ mod tests {
)
.await;
let url_upload_path = PasteType::OneshotUrl.get_path(&config.server.upload_path);
let url_upload_path = PasteType::OneshotUrl
.get_path(&config.server.upload_path)
.expect("Bad upload path");
fs::create_dir_all(&url_upload_path)?;
let response = test::call_service(

View File

@ -2,6 +2,7 @@ use crate::paste::PasteType;
use actix_web::{error, Error as ActixError};
use glob::glob;
use lazy_regex::{lazy_regex, Lazy, Regex};
use path_clean::PathClean;
use ring::digest::{Context, SHA256};
use std::fmt::Write;
use std::io::{BufReader, Read};
@ -37,7 +38,7 @@ pub fn glob_match_file(mut path: PathBuf) -> Result<PathBuf, ActixError> {
);
if let Some(glob_path) = glob(&format!("{}.[0-9]*", path.to_string_lossy()))
.map_err(error::ErrorInternalServerError)?
.next()
.last()
{
let glob_path = glob_path.map_err(error::ErrorInternalServerError)?;
if let Some(extension) = glob_path
@ -64,7 +65,8 @@ pub fn get_expired_files(base_path: &Path) -> Vec<PathBuf> {
PasteType::OneshotUrl,
]
.into_iter()
.filter_map(|v| glob(&v.get_path(base_path).join("*.[0-9]*").to_string_lossy()).ok())
.filter_map(|v| v.get_path(base_path).ok())
.filter_map(|v| glob(&v.join("*.[0-9]*").to_string_lossy()).ok())
.flat_map(|glob| glob.filter_map(|v| v.ok()).collect::<Vec<PathBuf>>())
.filter(|path| {
if let Some(extension) = path
@ -108,6 +110,27 @@ pub fn sha256_digest<R: Read>(input: R) -> Result<String, ActixError> {
})?)
}
/// Joins the paths whilst ensuring the path doesn't drastically change.
/// `base` is assumed to be a trusted value.
pub fn safe_path_join<B: AsRef<Path>, P: AsRef<Path>>(base: B, part: P) -> IoResult<PathBuf> {
let new_path = base.as_ref().join(part).clean();
let cleaned_base = base.as_ref().clean();
if !new_path.starts_with(cleaned_base) {
return Err(IoError::new(
IoErrorKind::InvalidData,
format!(
"{} is outside of {}",
new_path.display(),
base.as_ref().display()
),
));
}
Ok(new_path)
}
#[cfg(test)]
mod tests {
use super::*;
@ -170,4 +193,24 @@ mod tests {
assert_eq!(Vec::<PathBuf>::new(), get_expired_files(&current_dir));
Ok(())
}
#[test]
fn test_safe_join_path() {
assert_eq!(safe_path_join("/foo", "bar").ok(), Some("/foo/bar".into()));
assert_eq!(safe_path_join("/", "bar").ok(), Some("/bar".into()));
assert_eq!(safe_path_join("/", "././bar").ok(), Some("/bar".into()));
assert_eq!(
safe_path_join("/foo/bar", "baz/").ok(),
Some("/foo/bar/baz/".into())
);
assert_eq!(
safe_path_join("/foo/bar/../", "baz").ok(),
Some("/foo/baz".into())
);
assert!(safe_path_join("/foo", "/foobar").is_err());
assert!(safe_path_join("/foo", "/bar").is_err());
assert!(safe_path_join("/foo/bar", "..").is_err());
assert!(safe_path_join("/foo/bar", "../").is_err());
}
}