7

I'm trying to add a new line to the beginning of a text file. I start by opening the file with append but that only allows me to use write_all to write to the end of the file, at least that's the result I'm getting. If I read the documentation correctly, this is by design.

I've tried to play with seek, but that didn't solve it.

This is what I have currently:

let mut file = OpenOptions::new().append(true).open(&file_path).unwrap();
file.seek(SeekFrom::Start(0));
file.write_all(b"Cool days\n");

If I open the file with write, I end up overriding data instead of adding. What would be the appropriate way to go about accomplishing this with Rust?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
pedronveloso
  • 915
  • 9
  • 16
  • If you add a line to the beginning of a file, that will require moving all of the remainder of the contents of the file, to accommodate it. There isn't a way around that in any programming language. See [this example in C#](http://stackoverflow.com/questions/1343044/c-prepending-to-beginning-of-a-file). – Peter Hall Apr 16 '17 at 20:22

2 Answers2

19

You can't do this directly in any programming language. See some other questions on the same topic in C#, Python, NodeJs, PHP, Bash and C.

There are several solutions with different trade-offs:

  1. Copy the entire file into memory, write the data you want, and then write the rest of the file after it. If the file is large, this might be a bad solution because of the amount of memory it will use, but might be suitable for small files, because it is simple to implement.

  2. Use a buffer, the same size as the text you want to prepend. Copy chunks of the file at a time into memory and then overwrite it with the previous chunk. In this way, you can shuffle the contents of the file along, with the new text at the start. This is likely to be slower than the other approaches, but will not require a large memory allocation. It could also be the best choice when the process doesn't have permission to delete the file. But be careful: If the process is interrupted, this approach could leave the file in a corrupted state.

  3. Write the new data to a temporary file and then append the contents of the original. Then delete the original and rename the temporary file. This is a good solution because it delegates the heavy lifting to the operating system, and the original data is backed up so will not be corrupted if the process is interrupted.

From searching on Stack Overflow, the third solution seems to be the most popular answer for other languages, e.g. in Bash. This is likely to be because it is fast, safe and can often be implemented in just a few lines of code.

A quick Rust version looks something like this:

extern crate mktemp;
use mktemp::Temp;
use std::{fs, io, io::Write, fs::File, path::Path};

fn prepend_file<P: AsRef<Path>>(data: &[u8], file_path: &P) -> io::Result<()> {
    // Create a temporary file 
    let mut tmp_path = Temp::new_file()?;
    // Stop the temp file being automatically deleted when the variable
    // is dropped, by releasing it.
    tmp_path.release();
    // Open temp file for writing
    let mut tmp = File::create(&tmp_path)?;
    // Open source file for reading
    let mut src = File::open(&file_path)?;
    // Write the data to prepend
    tmp.write_all(&data)?;
    // Copy the rest of the source file
    io::copy(&mut src, &mut tmp)?;
    fs::remove_file(&file_path)?;
    fs::rename(&tmp_path, &file_path)?;
    Ok(())
}

Usage:

fn main() -> io::Result<()> {
    let file_path = Path::new("file.txt");
    let data = "Data to add to the beginning of the file\n";
    prepend_file(data.as_bytes(), &file_path)?;
    Ok(())
}
Peter Hall
  • 53,120
  • 14
  • 139
  • 204
  • Thank you, this answers my question. I was imagining a solution of that sort but though to myself it felt like reinventing the wheel, but apparently this wheel really needs to be made again. This would make for a great lib btw. – pedronveloso Apr 17 '17 at 10:32
0

An example with 4 steps: (Peter's solution #1)

  • create a vec holding data to preprend
  • open file, read content and append to vec
  • create file with same path, this will truncate (see doc File::create)
  • write content
use std::fs::File;
use std::path::Path;
use std::io::{Read, Write, Result};

fn prepend_file<P: AsRef<Path> + ?Sized>(data: &[u8], path: &P) -> Result<()> {
    let mut f =  File::open(path)?;
    let mut content = data.to_owned();
    f.read_to_end(&mut content)?;

    let mut f = File::create(path)?;
    f.write_all(content.as_slice())?;

    Ok(())
}

fn main() -> Result<()> {
    prepend_file("hello world\n".as_bytes(), "/tmp/file.txt")
}

Peter's solution works perfect, but it brings an extra crate, when dealing with small files, it's good to avoid it.

CtheSky
  • 2,484
  • 14
  • 16