Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TempFile persist_to fails with I/O Error 18 (Invalid cross-device link) #1600

Closed
thacoon opened this issue Mar 31, 2021 · 5 comments
Closed
Labels
docs Improvements or additions to documentation upstream An unresolvable issue: an upstream dependency bug

Comments

@thacoon
Copy link

thacoon commented Mar 31, 2021

I have the following code where I have a form with a TempFile which I want to save.

#[macro_use] extern crate rocket;

use rocket::data::TempFile;
use rocket::form::{Form};
use rocket_contrib::uuid::Uuid;

#[derive(FromForm)]
struct FileUploadForm<'f> {
    id: Uuid,
    file: TempFile<'f>,
}

#[post("/upload", data = "<form>")]
async fn upload(mut form: Form<FileUploadForm<'_>>) -> std::io::Result<()> {
    form.file.persist_to("/home/username/Example/test.jpg").await?;
    Ok(())
}

fn rocket() -> rocket::Rocket {
    rocket::ignite().mount("/", routes![upload])
}

#[rocket::main]
async fn main() {
    rocket()
        .launch()
        .await;
}

However, the code fails with the following error message: I/O Error: Os { code: 18, kind: Other, message: "Invalid cross-device link" }. I think, this is because my source (/tmp/) and my destination (/home/username/Example) have different mounts. If I change my source to the same mount, via export TMPDIR=/home/username/Example/tmp/ it works.

Does the code behaves as intended? Because I would argue that it should not fail because of the tmp dir being on a different mount than my persistent destination.

I am using the 0.5.0-dev version.

@jebrosen
Copy link
Collaborator

jebrosen commented Mar 31, 2021

Ouch. Yes, it looks like the underlying code ( https://docs.rs/tempfile/3.2.0/src/tempfile/file/imp/unix.rs.html#112 ) uses either rename or link, neither of which can cross filesystems mount points on Linux and probably other operating systems.

@jebrosen jebrosen added bug Deviation from the specification or expected behavior upstream An unresolvable issue: an upstream dependency bug labels Mar 31, 2021
@SergioBenitez
Copy link
Member

I'd conjecture that the underlying reason for this issue is the desire/need for renaming/relinking to be atomic. I'm not sure if we're liable to find a solution for this issue with the same security properties.

One "workaround" is to set your temporary directory to be in the other mount point:

[default]
temp_dir = "/home/tmp"

@SergioBenitez SergioBenitez added docs Improvements or additions to documentation and removed bug Deviation from the specification or expected behavior labels Apr 1, 2021
@SergioBenitez
Copy link
Member

Perhaps we should provide a copy_to() method, which will work across mount-points.

ShaddyDC added a commit to ShaddyDC/track-wear-backend that referenced this issue Jun 16, 2022
@jiangxiaoqiang
Copy link

did you solved this issue? @thacoon

@thacoon
Copy link
Author

thacoon commented Oct 3, 2023

@jiangxiaoqiang I just checked for you.

I was using it in a small side project 2 years ago.

My API looks like this where I am using persist_to:

#[post("/upload", data = "<form>")]
pub async fn upload(
    mut form: Form<FileUploadForm<'_>>,
    config: &State<config::Config>,
    _key: security::ApiKey,
) -> JsonValue {
    let mime_type;
    match extract_file_type(form.file.path()) {
        Some(m) => mime_type = m,
        None => return error_as_json(INVALID_FILE_TYPE),
    }

    if form
        .allowed_file_types
        .contains(&mime_type.to_string())
        .not()
    {
        return error_as_json(FILE_TYPE_NOT_ALLOWED);
    }

    let filename = format!("{}.{}", form.id.to_string(), mime_type.extension().unwrap());
    let path = Path::new(&config.media_root).join(filename.clone());

    if form.file.persist_to(&path).await.is_err() {
        return error_as_json(FILE_COULD_NOT_BE_PERSISTED);
    }

    json!({"status": "ok", "resource": filename})
}

And in my Rocket.toml I have:

[default]
api_key = "secret-api-key"
temp_dir = "./media/tmp"
media_root = "./media"

[debug]
api_key = "secret-api-key"
temp_dir = "./media/tmp"
media_root = "./media"

[release]
address = "0.0.0.0"
port = 8000
api_key = "secret-api-key"
temp_dir = "./media/tmp"
media_root = "./media"

[global.limits]
data-form = "5MiB"
file = "5MiB"

So, I think I am using the workaround mentioned by Sergio, that my source and destination are under the same mount.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Improvements or additions to documentation upstream An unresolvable issue: an upstream dependency bug
Projects
None yet
Development

No branches or pull requests

4 participants