Repository: diskfs/go-diskfs
Pull request #265 (2024-11-09 12:00:49)

Allows to open disk from already existing fs.File descriptor
I.e. in some `fs.FS` compliant VFS, given that `vfs.Open("some.img")` returns `fs.File`

@deitch if you have no objections, would you mind merging this improvement as well?

deitch: It looks like this is identical to `diskfs.Open()`, except that it passes an `fs.File` rather than a `string` path to the device or file to open?

I didn't understand the need for it.

> in some fs.FS compliant VFS, given that vfs.Open("some.img") returns fs.File

If that is the case, why `OpenFile(f fs.File)` and not `OpenFile(f *os.File)`? In the end, `Open()` [calls `os.OpenFile()`](https://github.com/diskfs/go-diskfs/pull/265/files#diff-9c737fb97131b341e11a4a47cb8eaf0c6cd1fd2ecf5c33bb2ae934e346c7eaf2R328) to get an `*os.File` anyways.
aol-nnov: > In the end, Open() calls os.OpenFile() to get an *os.File anyways.

But, actually, any implementation will fit as long as it supports fs.File + [util.File](https://github.com/diskfs/go-diskfs/blob/master/util/file.go#L9), right? To be frank, I had a thought to replace [this field](https://github.com/diskfs/go-diskfs/blob/master/disk/disk.go#L25) with interface, too, but spent too little time elaborating on it. So, to start with I decided to add just diskfs.OpenFile()

> why OpenFile(f fs.File) and not OpenFile(f *os.File)?

the only intent to use fs.File as argument is to hide type assertion inside `OpenFile()` implementation. If you insist, I can live with the assertion on my side...
deitch: > But, actually, any implementation will fit as long as it supports fs.File + [util.File](https://github.com/diskfs/go-diskfs/blob/master/util/file.go#L9), right?

Not quite. I almost responded that anything that supports `ReaderAt`, `WriterAt` and `Closer`, but that is not quite correct.

If you look at [`initDisk`](https://github.com/diskfs/go-diskfs/blob/f66ea89a8100199ca22395f32c4d7425e11040bb/diskfs.go#L188), it actually does an `f.Stat()` in order to get the size, determine if it is a file or block device, etc.

However, `Stat()` also is a [property of `fs.File`](https://pkg.go.dev/io/fs#File), so that might work. On the other hand, `fs.File` explicitly is read-only. I once did a whole implementation of "writable FS" for an OSS project.

If you want to see if it is possible to replace `Open(f *os.File)` with `Open(f SomeFIleType)` where:

```go
type SomeFIleType interface {
    fs.File
    ReaderAt
    WriterAt
    Closer
}
```

(but much better naming), then it is worth a try. But I wouldn't break `Open()`, instead have some other function that calls it, and unify them.
aol-nnov: > But I wouldn't break Open()

Definitely! That's why I came up with a new function for that.

>  to replace Open(f *os.File) with Open(f SomeFIleType)

I'd rather keep `SomeFIleType` internal and have public api as I proposed in the PR: `OpenFile(f fs.File, ...)` and assert type internally (and return an error if incoming type does not fit). That builds more smooth API as far as I can tell.

To cut the long story short, my typical usage is:

```go
myVfs := magicFs.New(...)
img, err:= myVfs.Open("disk.img")
disk, err:= diskfs.OpenFile(img)
```
That's why I want `diskfs.OpenFile()` accept `fs.File` as input.

I will investigate more and get back to you with my findings.
deitch: The problem is that `fs.File` is read only, so it doesn't work. If you really need to use just fs.File, probably best off casting it outside of the Open call. 

I like the idea of an interface rather than an fs.File, but it has to have the necessary functions. 
aol-nnov: > fs.File is read only, so it doesn't work

But it works ~~on my machine~~ in my scenario )) (and, yes, to be frank, my scenario is for reading only - I didn't check any writing functionality)

Okay, I've got your points. I need some time to think out the ways...
aol-nnov: @deitch I had some mocking attempts during the day, but it appeared a rabbit hole to replace os.File with interface, I must admit.

At some point I thought it could be worth having a read-only backing file interface for read operations and rw one for write operations.

Again, why do I stick to fs.File as the base: it is the common interface for all cases which we could type-assert the underlying storage for our needs.

So, I've created a storage class that has methods to type-assert to a required functionality and used it all over the place. Changes seem massive and not yet complete.

I just wanted to ask, if it's the right investment.. (please see my brain dump that I've offloaded today into this PR)
If not, I'd rather stick to my initial initiative with your corrections - add a single method `diskfs.OpenFile()` with the following:

```go
func OpenFile(f *os.File, opts ...OpenOpt) (*disk.Disk, error) {

	opt := &openOpts{
		mode:       ReadOnly,
		sectorSize: SectorSizeDefault,
	}

	for _, o := range opts {
		if err := o(opt); err != nil {
			return nil, err
		}
	}

	return initDisk(f, opt.mode, opt.sectorSize)
}
```

and perform all needed assertions outside. I'd be more than happy with that.

deitch: I am trying to wrap my head around what you did. I think I see something interesting, but I am not sure. Let's see if I get it.

* `diskfs.Open()` remains unchanged
* `diskfs.OpenFile()` is new. It takes as first parameter `fs.File`, and passes it to `initDisk()`
* `initDisk()` takes an `fs.File` as first parameter, instead of `*os.File`. 
* Rather than storing `Disk.Writable` as a separate property, it has a new structure called `storage.Disk` that wraps the underlying disk device, as well as storing the writable state
* Whenever we otherwise would have tried to write to the device, rather than checking `d.Writeable`, we call `d.File.Rw()`. If it returns `RwFile`, we know it is read-write, and then we can use it, otherwise return an error.  For things where we do not need to read, we can call `d.File.Ro()` and get `RoFile`.
* Lots of underlying functions get passed this `RoFile` or `RwFile` rather than whatever they had before, I guess for stricter type-checking.

The goals appear to be:

* Have the ability to pass an `fs.File`, which is more universal than `os.File`
* Have the ability to pass an `fs.File`, a read-only file structure, when we only will do read operations

Is that about it? If so, I think I like the goals, and most of the direction. I would suggest some simplifications, but let's wait until I see if I understood you correctly.
deitch: OK, got it. I have some thoughts on simplifications. They might or might not be worth it, once I type them out. I will get to them a bit later.
aol-nnov: Whenever you're comfortable to! No rush. I'd be glad to hear your suggestions! To be frank, that massive plot twist during my coding session was unexpected, but yet, I decided to share my thoughts ))

 I'm leaving for today (I'm GMT+3) and will be back online in next 12 hours or so. I'll have occasional access from smartphone for next 4 hours, though.
deitch: I will comment here, although some of it may be inline as well.

### devcontainer

I don't mind have `devcontainer.json` for it, but it should be in a different PR. I also would likely use the standard `library/golang`.

### storage.File

Originally, I didn't like the idea of it being in a new package, but I have come around to it.

The language of `Ro` vs `Rw` is somewhat misleading. All underlying storage at least must support read; some functions require it to be write as well. So maybe `storage.File` should be readable as is, while you can get a "writable variant" if you ask for one. Something like (I know this isn't 100% correct, just the idea):

```go
type File struct {
	fs.File
	io.ReaderAt
	io.Seeker
	io.Closer
}


type Writable interface {
	File
	io.WriterAt
}

// OS-stecific file for ioctl calls via fd
func (f File) Os() (*os.File, error) {
	if osFile, ok := f.storage.(*os.File); ok {
		return osFile, nil
	}
	return nil, errNotSuitable
}

// file for read-only operations is not needed, by definition I can use `storage.File` for anything readable

// file for writable operations
func (f File) Writable() (Writable, error) {
	if rwFile, ok := f.storage.(Writable); ok {
		if !f.readonly {
			return rwFile, nil
		} else {
			return nil, errIncorrectOpenMode
		}
	}
	return nil, errNotSuitable
}
```

So my usage becomes:

```go
func (d *Disk) ReadPartitionContents(part int, writer io.Writer) (int64, error) {
        /* this all goes away, since d.File _is_ readable
	backingRoFile, err := d.File.Ro()
	if err != nil {
		return -1, err
	}
        */
...
	return partitions[part-1].ReadContents(d.File, writer)
```

### usage of struct vs interface

I think this only appears in one place, but changing this:

```go
func (fs *FileSystem) readMetadata(r io.ReaderAt, c Compressor, firstBlock int64, initialBlockOffset uint32, byteOffset uint16, size int) ([]byte, error) {
```

to that:

```go
func (fs *FileSystem) readMetadata(r storage.RoFile, c Compressor, firstBlock int64, initialBlockOffset uint32, byteOffset uint16, size int) ([]byte, error) {
```

doesn't really buy anything. The only part of `storage.RoFile` (or just `storage.File`) that `readMetadata()` needs is its `io.ReaderAt`, so might as well leave it as the most basic interface needed.

### initDisk

I like what you did there. Why pass it an `*os.File` when a more abstract `fs.File` will do. Since `os.File` implements `fs.File`, it can be called in the same way without changing the calling function. Much cleaner. Use the most basic interface needed (similar to the point in the previous section).

Thoughts?
aol-nnov: Thank you for the detailed comments, Avi!

Devcontainer configs (and I've added some vscode configs later, too) definitely deserve a separate PR. Will split that into separate one in a moment. As for container image, it was offered by vscode wizard, so I suppose it contains some vscode-related tweaks for more comfortable development. I didn't dig any further in that direction yet.

As for newly introduced `storage` package, it was needed to avoid import cycles. At start I was trying to avoid it as much as I could, but failed.

I've got your idea about `storage.File` improvements and will proceed in that direction, thank you.

Regarding usage of struct vs interface in `readMetadata` and such, totally agreed. I will revert that part. It was a collateral damage of my fight with red underlines (type mismatches) all over the place during my intial attempt. Did not think much during that fighting, ha )

aol-nnov: Hey, @deitch ! Had some time to implement your suggestions today. Please, check if I'm moving in the right direction and got everything right.

Tests are still failing, but I'm on it. Just syncing and showing you my progress.
deitch: Yeah, that is pretty nice. Cleaner and easier to understand. It took more work, but I like it.
aol-nnov: Glad you like it!

I'll fix the tests then and we'll see it in action.

Btw, while separating ro and rw operations I've stumbled upon a fat32 test, where traversing directories actually was creating them. Now it's failing and I temporary commented it out. Needs more thinking! ))
deitch: > Btw, while separating ro and rw operations I've stumbled upon a fat32 test, where traversing directories actually was creating them. Now it's failing and I temporary commented it out. Needs more thinking! 

We have had cases like that before. Do you mind opening a separate issue just around the fat32 test and how to recreate it?
aol-nnov: > Do you mind opening a separate issue just around the fat32 test

(putting in my to-do list) Will do, eventually! ))) I haven't yet dove into concrete implementations yet.
aol-nnov: Oh, wow...

To my surprise, the similar plot is rolling out on the qcow2 branch! Just noticed it and now the puzzle fits quite well, yay!

May be we should tweak the naming here, too? It's no longer a `d.File`, it's something more generic!

It's now something like a "backend storage". `Backend`? Or, may be a `Driver` as you named it those days?
deitch: > the similar plot is rolling out on the qcow2 branch! Just noticed it and now the puzzle fits quite well, yay

Do you mean the test directory duplication issue?

> It's now something like a "backend storage". Backend? Or, may be a Driver as you named it those days?

No objection. I don't really think it is a driver, as it doesn't include lots of logic. It is a storage or backing store or backend.
aol-nnov: > Do you mean the test directory duplication issue?

No, I mean the whole interface-instead-of-`os.File` thing! ))
aol-nnov: Once in a while I'm thinking to myself, isn't it a time for v2, ha! ))
deitch: > P.S.: I don't get it.. Why does it fail only on ubuntu?! Darwin has the same tests, no?!

Because the test step [only runs on ubuntu](https://github.com/diskfs/go-diskfs/actions/runs/11871313640/workflow?pr=265#L70-L72):

```yaml
      - name: test
        if: matrix.os == 'ubuntu-latest'
        run: make test
```
deitch: Actually, this is looking pretty good. I do not know the issue with fat32 mkdir, but it looks like it has to do with the FSIS:

```
failed to write the file system information sector: disk file or device not open for write
```

Is it possible that the writablefile is not being passed through somewhere, or that writing FSIS is using just File?
aol-nnov: > Is it possible that the writablefile is not being passed through

Yeah, that was the case!

Phew, seems, we're on the homestretch with this change!

Should we add some sugar like

```go
disk, err := diskfs.WithBackend(raw.New(...))
```

Also, should we add any `Resize()` or `Create()` to `backend.Storage` interface?

Btw, I do not mind if you merge fat32 improvements first. I'll rebase on top of that.
deitch: Heh, already did merge those fat32 improvements. 😁 
aol-nnov: Rebase was super smooth - no conflicts!
deitch: How is this looking from your perspective? I see it still is draft, but all checks pass.

> Also, should we add any Resize() or Create() to backend.Storage interface?

I don't think so. Resize can only happen in certain circumstances, and it is messy, I don't want us to complicate things.

> Should we add some sugar like `disk, err := diskfs.WithBackend(raw.New(...))
> instead of newly introduced diskfs.OpenFile()?

Usually `With*()` is used for variadic functional arguments, like:

```go
foo.Create(foo.WithOpt1(), foo.WithSomeOtherOpt(), etc.)
```

We actually do that for [OpenOpt](https://pkg.go.dev/github.com/diskfs/go-diskfs#OpenOpt), so not `diskfs.WithBackend()`, but if you are thinking the same idea with different naming, maybe?

Right now, you get a disk via one of:

* `diskfs.Create(pathToFileOrDevice)`
* `diskfs.Open(pathToFileOrDevice)`

It intentionally mirrors [os.Create()](https://pkg.go.dev/os#Create) and [os.Open()](https://pkg.go.dev/os#Open), although I think we have shown the limits of that. There is nothing parallel to `Storage` in `os`, which isn't surprising.

Maybe `diskfs.OpenStorage()`?
aol-nnov: > How is this looking from your perspective?

I think, it's ready overall, just final touches left that I've pointed in my previous comment.

> Resize can only happen in certain circumstances, and it is messy

Actually, by introducing `Resize()` I was trying to address [this](https://github.com/diskfs/go-diskfs/blob/master/examples/create-iso-from-folder/main.go#L40) and similar situations.

Ideally, I was planning to move away from `diskfs.Open()`/`diskfs.Create()` and offload this operations to the `backend` implementation completely. (That's why I was talking about my v2 thoughts recently)

> Maybe `diskfs.OpenStorage()`?

Well, seems legit if you're not ready to drop current `diskfs.Open()`/`diskfs.Create()` functions.

Also, I've made attempt to revive qcow2 branch (although, I do not know much about that format) by snitching some code from that branch and making it fit newly introduced interface. Please [take a look](https://github.com/aol-nnov/go-diskfs/tree/revive-qcow2). I'd really like to keep the original authorship of that code, so, if you like it, could you refresh that branch yourself?
deitch: > Actually, by introducing Resize() I was trying to address [this](https://github.com/diskfs/go-diskfs/blob/master/examples/create-iso-from-folder/main.go#L40) and similar situations

I get it. In that case, let's get this one in, as it is a big change. Whether or not we want to support resize can be its own conversation in a separate issue or PR. Make sense? 

> Ideally, I was planning to move away from diskfs.Open()/diskfs.Create() and offload this operations to the backend implementation completely. (That's why I was talking about my v2 thoughts recently)
> Well, seems legit if you're not ready to drop current diskfs.Open()/diskfs.Create() functions

So what would it look like then? diskfs always will have `Open` and `Create`, to distinguish between "open an existing filesystem" on this storage, and "create a new filesystem" on this storage. Difference between `mount` and `mkfs`, essentially.

> Also, I've made attempt to revive qcow2 branch (although, I do not know much about that format) by snitching some code from there and making it fit newly introduced interface. Please [take a look](https://github.com/aol-nnov/go-diskfs/tree/revive-qcow2). I'd really like to keep the original authorship of that code, so, if you like it, could you refresh that branch yourself

Yeah, that one slipped. ext4 became more important, and got the final push through. It still is missing some functionality, but moved ahead a lot, enough to be functional.

Let's look at qcow2 as a separate question. I won't 100% have time for that for a bit.
deitch: The nice thing about this repo is that the tests run fairly quickly (unlike some other projects with which I am involved). So separating work into multiple PRs is low overhead.
aol-nnov: > Whether or not we want to support resize can be its own conversation in a separate issue or PR

Totally agreed. 

>  diskfs always will have Open and Create, to distinguish between "open an existing filesystem" on this storage, and "create a new filesystem" on this storage.

Controversial situation, indeed. I was [trying to introduce](https://github.com/aol-nnov/go-diskfs/blob/open-file/backend/raw/raw.go#L28) some sort of "constructor" for "creating a new image" as opposed to "constructor" for [existing device/image](https://github.com/aol-nnov/go-diskfs/blob/open-file/backend/raw/raw.go#L18) but I must admit, it's still not that expressive as it should be...

As for qcow2, I'm not pushing you in that direction by any means - just noticed the stalled branch and decided to check if it actually fits new reality, ha!
deitch: > Yeah! I'm really enjoying the speed!
> In some projects it takes from 30 minutes to hours to complete the PR pipeline - so frustrating!...

If you _really_ want and think it is important, I am sure I could come up with some way to make the CI pipeline take hours.
aol-nnov: >  I could come up with some way to make the CI pipeline take hours.

Hope, you're kidding me now, Avi!.. ))
aol-nnov: So, `OpenStorage` it is or are you planning to give more comments on [that](https://github.com/diskfs/go-diskfs/pull/265#issuecomment-2482338438)?
aol-nnov: What a harsh linter! Does not allow to use deprecated methods, ha!.. Well, okay, I'll soften the wordings....
aol-nnov: >  Is there a parallel for Create?

Not more than `disk.OpenStorage(raw.CreateFromPath("..."))`, as I stated [earlier](https://github.com/diskfs/go-diskfs/pull/265#issuecomment-2482338438).

If you have better ideas, please let me know!
deitch: > Not more than disk.OpenStorage(raw.CreateFromPath("...")), as I stated https://github.com/diskfs/go-diskfs/pull/265#issuecomment-2482338438.

Are you saying that `diskfs.Create()` is not really needed? [Here is the source](https://github.com/diskfs/go-diskfs/blob/v1.4.2/diskfs.go#L305-L355) for `Open()` and `Create()`:

```go
func Open(device string, opts ...OpenOpt) (*disk.Disk, error) {
	err := checkDevice(device)
	if err != nil {
		return nil, err
	}

	opt := openOptsDefaults()
	for _, o := range opts {
		if err := o(opt); err != nil {
			return nil, err
		}
	}

	m, ok := openModeOptions[opt.mode]
	if !ok {
		return nil, errors.New("unsupported file open mode")
	}

	f, err := os.OpenFile(device, m, 0o600)
	if err != nil {
		return nil, fmt.Errorf("could not open device %s with mode %v: %w", device, m, err)
	}
	// return our disk
	return initDisk(f, ReadWriteExclusive, opt.sectorSize)
}

// Create a Disk from a path to a device
// Should pass a path to a block device e.g. /dev/sda or a path to a file /tmp/foo.img
// The provided device must not exist at the time you call Create()
func Create(device string, size int64, _ Format, sectorSize SectorSize) (*disk.Disk, error) {
	if device == "" {
		return nil, errors.New("must pass device name")
	}
	if size <= 0 {
		return nil, errors.New("must pass valid device size to create")
	}
	f, err := os.OpenFile(device, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0o666)
	if err != nil {
		return nil, fmt.Errorf("could not create device %s: %w", device, err)
	}
	err = os.Truncate(device, size)
	if err != nil {
		return nil, fmt.Errorf("could not expand device %s to size %d: %w", device, size, err)
	}
	// return our disk
	return initDisk(f, ReadWriteExclusive, sectorSize)
}
```

Are you saying that we don't really need `Create()`, it could be moved to `raw.CreateFromPath()`, because all it does is:

1. ensure that a path was provided - storage takes care of that
2. Open the file with `RDWR|EXCL|CREATE`, so it creates the file if it does not exist
3. Ensures the size is provided and then grow/shrink the file to that size
4. call `initDisk`

So you are saying, that should be outside of `diskfs` basic functions, part of storage - create a file, set its size - and then you just would call `Open()` like any other that exists?
aol-nnov: You caught my thought perfectly correct!

That's what I was trying to say while introducing `Resize()` and `Create()` [here](https://github.com/diskfs/go-diskfs/pull/265#issuecomment-2481322751).

But definitely, it all needs more thinking... If you're fine with merging this PR as it is now and proceed in the next PRs, I'm totally fine, too!
deitch: > Also, this change will introduce the API breakage, so we need to decide how to handle such a big change

This one? This doesn't break anything. 😕 
Oh, maybe `Create()`? Why break that?

> You caught my thought perfectly correct!

Then we should document it. Maybe add an example to show how to use the storage to Create or Open, and the main doc a bit?
aol-nnov: > This one? 

No, the one you proposed!

> and then you just would call Open() like any other that exists?

In such case we could introduce a single `diskfs.Open(b storage.Backend)` but golang does not support method overloading as C does. Thus the API breakage - `diskfs.Open` would have a totally different set of parameters..
aol-nnov: > Maybe add an example to show how to use the storage to Create or Open, and the main doc a bit?

Yeah, will do later today or tomorrow morning! Also, I've got an idea to add `raw.OpenFromPath("..")` to parallel `diskfs.Open`.

So, making it as draft again! ))


aol-nnov: Added some example usage and `raw.CreateFromPath()`/`raw.OpenFromPath()` utility methods.
aol-nnov: @deitch probably, I had to make it more clear, that I was done with it - too much 'draft'-toggling back and forth... So, this is it, finally, ha!

Please review when you have time and let me know if there are any more improvements needed.
deitch: Sorry, took me some time to get to it. Trying now.
aol-nnov: > Sorry, took me some time to get to it. Trying now.

No worries and, definitely, no rush!

I'll address your comments tomorrow. I'm a bit overwhelmed with U-Boot back-porting right now, ha ))
aol-nnov: @deitch  I've renamed `raw` backend to `file` as per you request. We're close to merge complete than ever before ))

Patch:
diff --git a/README.md b/README.md
index e230077..51b5ee9 100644
--- a/README.md
+++ b/README.md
@@ -16,10 +16,18 @@ Note: detailed go documentation is available at [godoc.org](https://godoc.org/gi
 ### Concepts
 `go-diskfs` has a few basic concepts:
 
+* Backend
 * Disk
 * Partition
 * Filesystem
 
+#### Backend
+Backend is a (relatively) thin layer which abstracts low-level read/write operations. Through a backend you can seamlessly operate different disk formats.
+
+Currently there is only one implementation - file.
+
+Use `file` backend to access block devices and raw image files.
+
 #### Disk
 A disk represents either a file or block device that you access and manipulate. With access to the disk, you can:
 
diff --git a/backend/file/file.go b/backend/file/file.go
new file mode 100644
index 0000000..ec7912e
--- /dev/null
+++ b/backend/file/file.go
@@ -0,0 +1,127 @@
+package file
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"io/fs"
+	"os"
+
+	"github.com/diskfs/go-diskfs/backend"
+)
+
+type rawBackend struct {
+	storage  fs.File
+	readOnly bool
+}
+
+// Create a backend.Storage from provided fs.File
+func New(f fs.File, readOnly bool) backend.Storage {
+	return rawBackend{
+		storage:  f,
+		readOnly: readOnly,
+	}
+}
+
+// Create a backend.Storage from a path to a device
+// Should pass a path to a block device e.g. /dev/sda or a path to a file /tmp/foo.img
+// The provided device/file must exist at the time you call OpenFromPath()
+func OpenFromPath(pathName string, readOnly bool) (backend.Storage, error) {
+	if pathName == "" {
+		return nil, errors.New("must pass device of file name")
+	}
+
+	if _, err := os.Stat(pathName); os.IsNotExist(err) {
+		return nil, fmt.Errorf("provided device/file %s does not exist", pathName)
+	}
+
+	openMode := os.O_RDONLY
+
+	if !readOnly {
+		openMode |= os.O_RDWR | os.O_EXCL
+	}
+
+	f, err := os.OpenFile(pathName, openMode, 0o600)
+	if err != nil {
+		return nil, fmt.Errorf("could not open device %s with mode %v: %w", pathName, openMode, err)
+	}
+
+	return rawBackend{
+		storage:  f,
+		readOnly: readOnly,
+	}, nil
+}
+
+// Create a backend.Storage from a path to an image file.
+// Should pass a path to a file /tmp/foo.img
+// The provided file must not exist at the time you call CreateFromPath()
+func CreateFromPath(pathName string, size int64) (backend.Storage, error) {
+	if pathName == "" {
+		return nil, errors.New("must pass device name")
+	}
+	if size <= 0 {
+		return nil, errors.New("must pass valid device size to create")
+	}
+	f, err := os.OpenFile(pathName, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0o666)
+	if err != nil {
+		return nil, fmt.Errorf("could not create device %s: %w", pathName, err)
+	}
+	err = os.Truncate(pathName, size)
+	if err != nil {
+		return nil, fmt.Errorf("could not expand device %s to size %d: %w", pathName, size, err)
+	}
+
+	return rawBackend{
+		storage:  f,
+		readOnly: false,
+	}, nil
+}
+
+// backend.Storage interface guard
+var _ backend.Storage = (*rawBackend)(nil)
+
+// OS-specific file for ioctl calls via fd
+func (f rawBackend) Sys() (*os.File, error) {
+	if osFile, ok := f.storage.(*os.File); ok {
+		return osFile, nil
+	}
+	return nil, backend.ErrNotSuitable
+}
+
+// file for read-write operations
+func (f rawBackend) Writable() (backend.WritableFile, error) {
+	if rwFile, ok := f.storage.(backend.WritableFile); ok {
+		if !f.readOnly {
+			return rwFile, nil
+		}
+
+		return nil, backend.ErrIncorrectOpenMode
+	}
+	return nil, backend.ErrNotSuitable
+}
+
+func (f rawBackend) Stat() (fs.FileInfo, error) {
+	return f.storage.Stat()
+}
+
+func (f rawBackend) Read(b []byte) (int, error) {
+	return f.storage.Read(b)
+}
+
+func (f rawBackend) Close() error {
+	return f.storage.Close()
+}
+
+func (f rawBackend) ReadAt(p []byte, off int64) (n int, err error) {
+	if readerAt, ok := f.storage.(io.ReaderAt); ok {
+		return readerAt.ReadAt(p, off)
+	}
+	return -1, backend.ErrNotSuitable
+}
+
+func (f rawBackend) Seek(offset int64, whence int) (int64, error) {
+	if seeker, ok := f.storage.(io.Seeker); ok {
+		return seeker.Seek(offset, whence)
+	}
+	return -1, backend.ErrNotSuitable
+}
diff --git a/backend/interface.go b/backend/interface.go
new file mode 100644
index 0000000..21e7b34
--- /dev/null
+++ b/backend/interface.go
@@ -0,0 +1,33 @@
+package backend
+
+import (
+	"errors"
+	"io"
+	"io/fs"
+	"os"
+)
+
+var (
+	ErrIncorrectOpenMode = errors.New("disk file or device not open for write")
+	ErrNotSuitable       = errors.New("backing file is not suitable")
+)
+
+type File interface {
+	fs.File
+	io.ReaderAt
+	io.Seeker
+	io.Closer
+}
+
+type WritableFile interface {
+	File
+	io.WriterAt
+}
+
+type Storage interface {
+	File
+	// OS-specific file for ioctl calls via fd
+	Sys() (*os.File, error)
+	// file for read-write operations
+	Writable() (WritableFile, error)
+}
diff --git a/disk/disk.go b/disk/disk.go
index 25d4172..230170a 100644
--- a/disk/disk.go
+++ b/disk/disk.go
@@ -8,28 +8,24 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"os"
-
-	log "github.com/sirupsen/logrus"
 
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/filesystem/ext4"
 	"github.com/diskfs/go-diskfs/filesystem/fat32"
 	"github.com/diskfs/go-diskfs/filesystem/iso9660"
 	"github.com/diskfs/go-diskfs/filesystem/squashfs"
 	"github.com/diskfs/go-diskfs/partition"
+	log "github.com/sirupsen/logrus"
 )
 
 // Disk is a reference to a single disk block device or image that has been Create() or Open()
 type Disk struct {
-	File              *os.File
-	Info              os.FileInfo
-	Type              Type
+	Backend           backend.Storage
 	Size              int64
 	LogicalBlocksize  int64
 	PhysicalBlocksize int64
 	Table             partition.Table
-	Writable          bool
 	DefaultBlocks     bool
 }
 
@@ -43,17 +39,13 @@ const (
 	Device
 )
 
-var (
-	errIncorrectOpenMode = errors.New("disk file or device not open for write")
-)
-
 // GetPartitionTable retrieves a PartitionTable for a Disk
 //
 // If the table is able to be retrieved from the disk, it is saved in the instance.
 //
 // returns an error if the Disk is invalid or does not exist, or the partition table is unknown
 func (d *Disk) GetPartitionTable() (partition.Table, error) {
-	t, err := partition.Read(d.File, int(d.LogicalBlocksize), int(d.PhysicalBlocksize))
+	t, err := partition.Read(d.Backend, int(d.LogicalBlocksize), int(d.PhysicalBlocksize))
 	if err != nil {
 		return nil, err
 	}
@@ -68,24 +60,19 @@ func (d *Disk) GetPartitionTable() (partition.Table, error) {
 //
 // Actual writing of the table is delegated to the individual implementation
 func (d *Disk) Partition(table partition.Table) error {
-	if !d.Writable {
-		return errIncorrectOpenMode
+	rwBackingFile, err := d.Backend.Writable()
+	if err != nil {
+		return err
 	}
+
 	// fill in the uuid
-	err := table.Write(d.File, d.Size)
+	err = table.Write(rwBackingFile, d.Size)
 	if err != nil {
 		return fmt.Errorf("failed to write partition table: %v", err)
 	}
 	d.Table = table
-	// the partition table needs to be re-read only if
-	// the disk file is an actual block device
-	if d.Type == Device {
-		err = d.ReReadPartitionTable()
-		if err != nil {
-			return fmt.Errorf("unable to re-read the partition table. Kernel still uses old partition table: %v", err)
-		}
-	}
-	return nil
+
+	return d.ReReadPartitionTable()
 }
 
 // WritePartitionContents writes the contents of an io.Reader to a given partition
@@ -95,8 +82,10 @@ func (d *Disk) Partition(table partition.Table) error {
 // returns an error if there was an error writing to the disk, reading from the reader, the table
 // is invalid, or the partition is invalid
 func (d *Disk) WritePartitionContents(part int, reader io.Reader) (int64, error) {
-	if !d.Writable {
-		return -1, errIncorrectOpenMode
+	backingRwFile, err := d.Backend.Writable()
+
+	if err != nil {
+		return -1, err
 	}
 	if d.Table == nil {
 		return -1, fmt.Errorf("cannot write contents of a partition on a disk without a partition table")
@@ -109,7 +98,7 @@ func (d *Disk) WritePartitionContents(part int, reader io.Reader) (int64, error)
 	if part > len(partitions) {
 		return -1, fmt.Errorf("cannot write contents of partition %d which is greater than max partition %d", part, len(partitions))
 	}
-	written, err := partitions[part-1].WriteContents(d.File, reader)
+	written, err := partitions[part-1].WriteContents(backingRwFile, reader)
 	return int64(written), err
 }
 
@@ -131,7 +120,7 @@ func (d *Disk) ReadPartitionContents(part int, writer io.Writer) (int64, error)
 	if part > len(partitions) {
 		return -1, fmt.Errorf("cannot read contents of partition %d which is greater than max partition %d", part, len(partitions))
 	}
-	return partitions[part-1].ReadContents(d.File, writer)
+	return partitions[part-1].ReadContents(d.Backend, writer)
 }
 
 // FilesystemSpec represents the specification of a filesystem to be created
@@ -162,9 +151,8 @@ func (d *Disk) CreateFilesystem(spec FilesystemSpec) (filesystem.FileSystem, err
 	var (
 		size, start int64
 	)
+
 	switch {
-	case !d.Writable:
-		return nil, errIncorrectOpenMode
 	case spec.Partition == 0:
 		size = d.Size
 		start = 0
@@ -183,11 +171,11 @@ func (d *Disk) CreateFilesystem(spec FilesystemSpec) (filesystem.FileSystem, err
 
 	switch spec.FSType {
 	case filesystem.TypeFat32:
-		return fat32.Create(d.File, size, start, d.LogicalBlocksize, spec.VolumeLabel)
+		return fat32.Create(d.Backend, size, start, d.LogicalBlocksize, spec.VolumeLabel)
 	case filesystem.TypeISO9660:
-		return iso9660.Create(d.File, size, start, d.LogicalBlocksize, spec.WorkDir)
+		return iso9660.Create(d.Backend, size, start, d.LogicalBlocksize, spec.WorkDir)
 	case filesystem.TypeExt4:
-		return ext4.Create(d.File, size, start, d.LogicalBlocksize, nil)
+		return ext4.Create(d.Backend, size, start, d.LogicalBlocksize, nil)
 	case filesystem.TypeSquashfs:
 		return nil, filesystem.ErrReadonlyFilesystem
 	default:
@@ -228,7 +216,7 @@ func (d *Disk) GetFilesystem(part int) (filesystem.FileSystem, error) {
 
 	// just try each type
 	log.Debug("trying fat32")
-	fat32FS, err := fat32.Read(d.File, size, start, d.LogicalBlocksize)
+	fat32FS, err := fat32.Read(d.Backend, size, start, d.LogicalBlocksize)
 	if err == nil {
 		return fat32FS, nil
 	}
@@ -238,17 +226,17 @@ func (d *Disk) GetFilesystem(part int) (filesystem.FileSystem, error) {
 		pbs = 0
 	}
 	log.Debugf("trying iso9660 with physical block size %d", pbs)
-	iso9660FS, err := iso9660.Read(d.File, size, start, pbs)
+	iso9660FS, err := iso9660.Read(d.Backend, size, start, pbs)
 	if err == nil {
 		return iso9660FS, nil
 	}
 	log.Debugf("iso9660 failed: %v", err)
-	squashFS, err := squashfs.Read(d.File, size, start, d.LogicalBlocksize)
+	squashFS, err := squashfs.Read(d.Backend, size, start, d.LogicalBlocksize)
 	if err == nil {
 		return squashFS, nil
 	}
 	log.Debug("trying ext4")
-	ext4FS, err := ext4.Read(d.File, size, start, d.LogicalBlocksize)
+	ext4FS, err := ext4.Read(d.Backend, size, start, d.LogicalBlocksize)
 	if err == nil {
 		return ext4FS, nil
 	}
@@ -258,7 +246,7 @@ func (d *Disk) GetFilesystem(part int) (filesystem.FileSystem, error) {
 
 // Close the disk. Once successfully closed, it can no longer be used.
 func (d *Disk) Close() error {
-	if err := d.File.Close(); err != nil {
+	if err := d.Backend.Close(); err != nil {
 		return err
 	}
 	*d = Disk{}
diff --git a/disk/disk_unix.go b/disk/disk_unix.go
index aba635c..1900f88 100644
--- a/disk/disk_unix.go
+++ b/disk/disk_unix.go
@@ -5,6 +5,7 @@ package disk
 
 import (
 	"fmt"
+	"os"
 
 	"golang.org/x/sys/unix"
 )
@@ -18,10 +19,24 @@ const (
 //
 // It is done via an ioctl call with request as BLKRRPART.
 func (d *Disk) ReReadPartitionTable() error {
-	fd := d.File.Fd()
-	_, err := unix.IoctlGetInt(int(fd), blkrrpart)
+	// the partition table needs to be re-read only if
+	// the disk file is an actual block device
+	devInfo, err := d.Backend.Stat()
 	if err != nil {
-		return fmt.Errorf("unable to re-read partition table: %v", err)
+		return err
 	}
+
+	if devInfo.Mode()&os.ModeDevice != 0 {
+		osFile, err := d.Backend.Sys()
+		if err != nil {
+			return err
+		}
+		fd := osFile.Fd()
+		_, err = unix.IoctlGetInt(int(fd), blkrrpart)
+		if err != nil {
+			return fmt.Errorf("unable to re-read the partition table. Kernel still uses old partition table: %v", err)
+		}
+	}
+
 	return nil
 }
diff --git a/diskfs.go b/diskfs.go
index 75fa536..420aa61 100644
--- a/diskfs.go
+++ b/diskfs.go
@@ -17,6 +17,8 @@ import (
 
 	log "github.com/sirupsen/logrus"
 
+	"github.com/diskfs/go-diskfs/backend"
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/disk"
 )
 
@@ -30,14 +32,6 @@ const (
 	// blkpbszGet                       = 0x127b
 )
 
-// Format represents the format of the disk
-type Format int
-
-const (
-	// Raw disk format for basic raw disk
-	Raw Format = iota
-)
-
 // OpenModeOption represents file open modes
 type OpenModeOption int
 
@@ -93,15 +87,13 @@ func writableMode(mode OpenModeOption) bool {
 	return false
 }
 
-func initDisk(f *os.File, openMode OpenModeOption, sectorSize SectorSize) (*disk.Disk, error) {
+func initDisk(b backend.Storage, sectorSize SectorSize) (*disk.Disk, error) {
+	log.Debug("initDisk(): start")
+
 	var (
-		diskType      disk.Type
-		size          int64
-		lblksize      = int64(defaultBlocksize)
-		pblksize      = int64(defaultBlocksize)
-		defaultBlocks = true
+		lblksize = int64(defaultBlocksize)
+		pblksize = int64(defaultBlocksize)
 	)
-	log.Debug("initDisk(): start")
 
 	if sectorSize != SectorSizeDefault {
 		lblksize = int64(sectorSize)
@@ -109,61 +101,63 @@ func initDisk(f *os.File, openMode OpenModeOption, sectorSize SectorSize) (*disk
 	}
 
 	// get device information
-	devInfo, err := f.Stat()
+	devInfo, err := b.Stat()
 	if err != nil {
-		return nil, fmt.Errorf("could not get info for device %s: %v", f.Name(), err)
+		return nil, fmt.Errorf("could not get info for device %s: %v", devInfo.Name(), err)
+	}
+
+	newDisk := &disk.Disk{
+		Backend:           b,
+		Size:              devInfo.Size(),
+		LogicalBlocksize:  lblksize,
+		PhysicalBlocksize: pblksize,
+		DefaultBlocks:     true,
 	}
+
 	mode := devInfo.Mode()
 	switch {
 	case mode.IsRegular():
 		log.Debug("initDisk(): regular file")
-		diskType = disk.File
-		size = devInfo.Size()
-		if size <= 0 {
-			return nil, fmt.Errorf("could not get file size for device %s", f.Name())
+		if newDisk.Size <= 0 {
+			return nil, fmt.Errorf("could not get file size for device %s", devInfo.Name())
 		}
 	case mode&os.ModeDevice != 0:
 		log.Debug("initDisk(): block device")
-		diskType = disk.Device
-		size, err = getBlockDeviceSize(f)
+		osFile, err := newDisk.Backend.Sys()
 		if err != nil {
-			return nil, fmt.Errorf("error getting block device %s size: %s", f.Name(), err)
+			return nil, backend.ErrNotSuitable
 		}
-		lblksize, pblksize, err = getSectorSizes(f)
-		log.Debugf("initDisk(): logical block size %d, physical block size %d", lblksize, pblksize)
-		defaultBlocks = false
-		if err != nil {
-			return nil, fmt.Errorf("unable to get block sizes for device %s: %v", f.Name(), err)
+
+		if size, err := getBlockDeviceSize(osFile); err != nil {
+			return nil, fmt.Errorf("error getting block device %s size: %s", devInfo.Name(), err)
+		} else {
+			newDisk.Size = size
+		}
+
+		if lblksize, pblksize, err = getSectorSizes(osFile); err != nil {
+			return nil, fmt.Errorf("unable to get block sizes for device %s: %v", devInfo.Name(), err)
+		} else {
+			log.Debugf("initDisk(): logical block size %d, physical block size %d", lblksize, pblksize)
+
+			newDisk.LogicalBlocksize = lblksize
+			newDisk.PhysicalBlocksize = pblksize
+			newDisk.DefaultBlocks = false
 		}
+
 	default:
-		return nil, fmt.Errorf("device %s is neither a block device nor a regular file", f.Name())
+		return nil, fmt.Errorf("device %s is neither a block device nor a regular file", devInfo.Name())
 	}
 
 	// how many good blocks do we have?
 	//    var goodBlocks, orphanedBlocks int
 	//    goodBlocks = size / lblksize
 
-	writable := writableMode(openMode)
-
-	ret := &disk.Disk{
-		File:              f,
-		Info:              devInfo,
-		Type:              diskType,
-		Size:              size,
-		LogicalBlocksize:  lblksize,
-		PhysicalBlocksize: pblksize,
-		Writable:          writable,
-		DefaultBlocks:     defaultBlocks,
-	}
-
 	// try to initialize the partition table.
-	// we ignore errors, because it is perfectly fine to open a disk
-	// and use it before it has a partition table. This is solely
-	// a convenience.
-	if table, err := ret.GetPartitionTable(); err == nil && table != nil {
-		ret.Table = table
-	}
-	return ret, nil
+	//nolint:errcheck // we ignore errors, because it is perfectly fine to open a disk and use it before it has a
+	// partition table. This is solely a convenience.
+	newDisk.GetPartitionTable()
+
+	return newDisk, nil
 }
 
 func checkDevice(device string) error {
@@ -210,6 +204,7 @@ func WithSectorSize(sectorSize SectorSize) OpenOpt {
 	}
 }
 
+// Might be deprecated in future: use <backend>.New + diskfs.OpenBackend
 // Open a Disk from a path to a device in read-write exclusive mode
 // Should pass a path to a block device e.g. /dev/sda or a path to a file /tmp/foo.img
 // The provided device must exist at the time you call Open().
@@ -236,28 +231,37 @@ func Open(device string, opts ...OpenOpt) (*disk.Disk, error) {
 	if err != nil {
 		return nil, fmt.Errorf("could not open device %s with mode %v: %w", device, m, err)
 	}
+
 	// return our disk
-	return initDisk(f, ReadWriteExclusive, opt.sectorSize)
+	return initDisk(file.New(f, !writableMode(opt.mode)), opt.sectorSize)
+}
+
+// Open a Disk using provided fs.File to a device in read-only mode
+// Use OpenOpt to control options, such as sector size or open mode.
+func OpenBackend(b backend.Storage, opts ...OpenOpt) (*disk.Disk, error) {
+	opt := &openOpts{
+		mode:       ReadOnly,
+		sectorSize: SectorSizeDefault,
+	}
+
+	for _, o := range opts {
+		if err := o(opt); err != nil {
+			return nil, err
+		}
+	}
+
+	return initDisk(b, opt.sectorSize)
 }
 
+// Might be deprecated in future: use <backend>.CreateFromPath + diskfs.OpenBackend
 // Create a Disk from a path to a device
 // Should pass a path to a block device e.g. /dev/sda or a path to a file /tmp/foo.img
 // The provided device must not exist at the time you call Create()
-func Create(device string, size int64, _ Format, sectorSize SectorSize) (*disk.Disk, error) {
-	if device == "" {
-		return nil, errors.New("must pass device name")
-	}
-	if size <= 0 {
-		return nil, errors.New("must pass valid device size to create")
-	}
-	f, err := os.OpenFile(device, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0o666)
+func Create(device string, size int64, sectorSize SectorSize) (*disk.Disk, error) {
+	rawBackend, err := file.CreateFromPath(device, size)
 	if err != nil {
-		return nil, fmt.Errorf("could not create device %s: %w", device, err)
-	}
-	err = os.Truncate(device, size)
-	if err != nil {
-		return nil, fmt.Errorf("could not expand device %s to size %d: %w", device, size, err)
+		return nil, err
 	}
 	// return our disk
-	return initDisk(f, ReadWriteExclusive, sectorSize)
+	return initDisk(rawBackend, sectorSize)
 }
diff --git a/examples/bootable_iso.go b/examples/bootable_iso.go
index f907a63..2c2589f 100644
--- a/examples/bootable_iso.go
+++ b/examples/bootable_iso.go
@@ -16,7 +16,7 @@ func CreateBootableIso(diskImg string) {
 		log.Fatal("must have a valid path for diskImg")
 	}
 	var diskSize int64 = 10 * 1024 * 1024 // 10 MB
-	mydisk, err := diskfs.Create(diskImg, diskSize, diskfs.Raw, diskfs.SectorSizeDefault)
+	mydisk, err := diskfs.Create(diskImg, diskSize, diskfs.SectorSizeDefault)
 	check(err)
 
 	// the following line is required for an ISO, which may have logical block sizes
diff --git a/examples/create-iso-from-folder/go.mod b/examples/create-iso-from-folder/go.mod
index b3fe818..b098c4f 100644
--- a/examples/create-iso-from-folder/go.mod
+++ b/examples/create-iso-from-folder/go.mod
@@ -1,15 +1,21 @@
 module main
 
-go 1.19
+go 1.22
+
+toolchain go1.23.2
+
+replace github.com/diskfs/go-diskfs => ../..
 
 require github.com/diskfs/go-diskfs v1.3.0
 
 require (
+	github.com/djherbis/times v1.6.0 // indirect
+	github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab // indirect
 	github.com/google/uuid v1.3.0 // indirect
-	github.com/pierrec/lz4 v2.6.1+incompatible // indirect
+	github.com/klauspost/compress v1.17.4 // indirect
+	github.com/pierrec/lz4/v4 v4.1.17 // indirect
 	github.com/pkg/xattr v0.4.9 // indirect
-	github.com/sirupsen/logrus v1.9.0 // indirect
+	github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
 	github.com/ulikunitz/xz v0.5.11 // indirect
-	golang.org/x/sys v0.5.0 // indirect
-	gopkg.in/djherbis/times.v1 v1.3.0 // indirect
+	golang.org/x/sys v0.19.0 // indirect
 )
diff --git a/examples/create-iso-from-folder/go.sum b/examples/create-iso-from-folder/go.sum
index a1727cf..bc7a3fc 100644
--- a/examples/create-iso-from-folder/go.sum
+++ b/examples/create-iso-from-folder/go.sum
@@ -1,52 +1,37 @@
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/diskfs/go-diskfs v1.3.0 h1:D3IVe1y7ybB5SjCO0pOmkWThL9lZEWeanp8rRa0q0sk=
-github.com/diskfs/go-diskfs v1.3.0/go.mod h1:3pUpCAz75Q11om5RsGpVKUgXp2Z+ATw1xV500glmCP0=
-github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk=
-github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
+github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
+github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
+github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
+github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
 github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
 github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/pierrec/lz4 v2.3.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
-github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
-github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
-github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs=
+github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
+github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
+github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
 github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
+github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
 github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-golang.org/x/sys v0.0.0-20181021155630-eda9bb28ed51/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/djherbis/times.v1 v1.2.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8=
-gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o=
-gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/examples/create-iso-from-folder/main.go b/examples/create-iso-from-folder/main.go
index e95f723..3f2e11f 100644
--- a/examples/create-iso-from-folder/main.go
+++ b/examples/create-iso-from-folder/main.go
@@ -45,8 +45,7 @@ func CreateIsoFromFolder(srcFolder string, outputFileName string) {
 	var LogicalBlocksize diskfs.SectorSize = 2048
 
 	// Create the disk image
-	// TODO: Explain why we need to use Raw here
-	mydisk, err := diskfs.Create(outputFileName, folderSize, diskfs.Raw, LogicalBlocksize)
+	mydisk, err := diskfs.Create(outputFileName, folderSize, LogicalBlocksize)
 	check(err)
 
 	// Create the ISO filesystem on the disk image
diff --git a/examples/efi_create.go b/examples/efi_create.go
index 40979ea..fda0478 100644
--- a/examples/efi_create.go
+++ b/examples/efi_create.go
@@ -25,7 +25,7 @@ func CreateEfi(diskImg string) {
 	)
 
 	// create a disk image
-	disk, err := diskfs.Create(diskImg, diskSize, diskfs.Raw, diskfs.SectorSizeDefault)
+	disk, err := diskfs.Create(diskImg, diskSize, diskfs.SectorSizeDefault)
 	if err != nil {
 		log.Panic(err)
 	}
diff --git a/examples/iso_create.go b/examples/iso_create.go
index 26f5bde..36ff123 100644
--- a/examples/iso_create.go
+++ b/examples/iso_create.go
@@ -16,7 +16,7 @@ func CreateIso(diskImg string) {
 		log.Fatal("must have a valid path for diskImg")
 	}
 	var diskSize int64 = 10 * 1024 * 1024 // 10 MB
-	mydisk, err := diskfs.Create(diskImg, diskSize, diskfs.Raw, diskfs.SectorSizeDefault)
+	mydisk, err := diskfs.Create(diskImg, diskSize, diskfs.SectorSizeDefault)
 	check(err)
 
 	// the following line is required for an ISO, which may have logical block sizes
diff --git a/examples/serve-image/main.go b/examples/serve-image/main.go
index d147a3d..5fff0d9 100644
--- a/examples/serve-image/main.go
+++ b/examples/serve-image/main.go
@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"os"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/filesystem/fat32"
 	"github.com/diskfs/go-diskfs/filesystem/iso9660"
@@ -22,15 +23,17 @@ func main() {
 	if err != nil {
 		log.Fatalf("Cannot open %q: %s", *filename, err)
 	}
+	b := file.New(f, true)
+
 	defer f.Close()
 	var fs filesystem.FileSystem
 	switch *fsType {
 	case "iso9660":
-		fs, err = iso9660.Read(f, 0, 0, 0)
+		fs, err = iso9660.Read(b, 0, 0, 0)
 	case "fat32":
-		fs, err = fat32.Read(f, 0, 0, 0)
+		fs, err = fat32.Read(b, 0, 0, 0)
 	case "squashfs":
-		fs, err = squashfs.Read(f, 0, 0, 0)
+		fs, err = squashfs.Read(b, 0, 0, 0)
 	default:
 		log.Fatalf("Unknown filesystem type %q", *fsType)
 	}
diff --git a/examples/squashfs_create.go b/examples/squashfs_create.go
index 7f7f2ad..6d1d127 100644
--- a/examples/squashfs_create.go
+++ b/examples/squashfs_create.go
@@ -16,7 +16,7 @@ func CreateSquashfs(diskImg string) {
 		log.Fatal("must have a valid path for diskImg")
 	}
 	var diskSize int64 = 10 * 1024 * 1024 // 10 MB
-	mydisk, err := diskfs.Create(diskImg, diskSize, diskfs.Raw, diskfs.SectorSizeDefault)
+	mydisk, err := diskfs.Create(diskImg, diskSize, diskfs.SectorSizeDefault)
 	check(err)
 
 	fspec := disk.FilesystemSpec{Partition: 0, FSType: filesystem.TypeSquashfs, VolumeLabel: "label"}
diff --git a/filesystem/ext4/ext4.go b/filesystem/ext4/ext4.go
index 8f326e6..7aa11c5 100644
--- a/filesystem/ext4/ext4.go
+++ b/filesystem/ext4/ext4.go
@@ -13,6 +13,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/filesystem/ext4/crc"
 	"github.com/diskfs/go-diskfs/util"
@@ -97,12 +98,12 @@ type FileSystem struct {
 	blockGroups      int64
 	size             int64
 	start            int64
-	file             util.File
+	backend          backend.Storage
 }
 
 // Equal compare if two filesystems are equal
 func (fs *FileSystem) Equal(a *FileSystem) bool {
-	localMatch := fs.file == a.file
+	localMatch := fs.backend == a.backend
 	sbMatch := fs.superblock.equal(a.superblock)
 	gdMatch := fs.groupDescriptors.equal(a.groupDescriptors)
 	return localMatch && sbMatch && gdMatch
@@ -110,8 +111,8 @@ func (fs *FileSystem) Equal(a *FileSystem) bool {
 
 // Create creates an ext4 filesystem in a given file or device
 //
-// requires the util.File where to create the filesystem, size is the size of the filesystem in bytes,
-// start is how far in bytes from the beginning of the util.File to create the filesystem,
+// requires the backend.Storage where to create the filesystem, size is the size of the filesystem in bytes,
+// start is how far in bytes from the beginning of the backend.Storage to create the filesystem,
 // and sectorsize is is the logical sector size to use for creating the filesystem
 //
 // blocksize is the size of the ext4 blocks, and is calculated as sectorsPerBlock * sectorsize.
@@ -131,7 +132,7 @@ func (fs *FileSystem) Equal(a *FileSystem) bool {
 // or 512, it will return an error.
 //
 //nolint:gocyclo // yes, this has high cyclomatic complexity, but we can accept it
-func Create(f util.File, size, start, sectorsize int64, p *Params) (*FileSystem, error) {
+func Create(b backend.Storage, size, start, sectorsize int64, p *Params) (*FileSystem, error) {
 	// be safe about the params pointer
 	if p == nil {
 		p = &Params{}
@@ -539,7 +540,7 @@ func Create(f util.File, size, start, sectorsize int64, p *Params) (*FileSystem,
 	}
 	gdt := groupDescriptors{}
 
-	b, err := sb.toBytes()
+	superblockBytes, err := sb.toBytes()
 	if err != nil {
 		return nil, fmt.Errorf("error converting Superblock to bytes: %v", err)
 	}
@@ -561,8 +562,13 @@ func Create(f util.File, size, start, sectorsize int64, p *Params) (*FileSystem,
 			incr = int64(SectorSize512) * 2
 		}
 
+		writable, err := b.Writable()
+		if err != nil {
+			return nil, err
+		}
+
 		// write the superblock
-		count, err := f.WriteAt(b, incr+blockStart+start)
+		count, err := writable.WriteAt(superblockBytes, incr+blockStart+start)
 		if err != nil {
 			return nil, fmt.Errorf("error writing Superblock for block %d to disk: %v", block, err)
 		}
@@ -571,7 +577,7 @@ func Create(f util.File, size, start, sectorsize int64, p *Params) (*FileSystem,
 		}
 
 		// write the GDT
-		count, err = f.WriteAt(g, incr+blockStart+int64(SuperblockSize)+start)
+		count, err = writable.WriteAt(g, incr+blockStart+int64(SuperblockSize)+start)
 		if err != nil {
 			return nil, fmt.Errorf("error writing GDT for block %d to disk: %v", block, err)
 		}
@@ -589,14 +595,14 @@ func Create(f util.File, size, start, sectorsize int64, p *Params) (*FileSystem,
 		blockGroups:      blockGroups,
 		size:             size,
 		start:            start,
-		file:             f,
+		backend:          b,
 	}, nil
 }
 
 // Read reads a filesystem from a given disk.
 //
-// requires the util.File where to read the filesystem, size is the size of the filesystem in bytes,
-// start is how far in bytes from the beginning of the util.File the filesystem is expected to begin,
+// requires the backend.File where to read the filesystem, size is the size of the filesystem in bytes,
+// start is how far in bytes from the beginning of the backend.File the filesystem is expected to begin,
 // and blocksize is is the logical blocksize to use for creating the filesystem
 //
 // note that you are *not* required to read a filesystem on the entire disk. You could have a disk of size
@@ -609,7 +615,7 @@ func Create(f util.File, size, start, sectorsize int64, p *Params) (*FileSystem,
 //
 // If the provided blocksize is 0, it will use the default of 512 bytes. If it is any number other than 0
 // or 512, it will return an error.
-func Read(file util.File, size, start, sectorsize int64) (*FileSystem, error) {
+func Read(b backend.Storage, size, start, sectorsize int64) (*FileSystem, error) {
 	// blocksize must be <=0 or exactly SectorSize512 or error
 	if sectorsize != int64(SectorSize512) && sectorsize > 0 {
 		return nil, fmt.Errorf("sectorsize for ext4 must be either 512 bytes or 0, not %d", sectorsize)
@@ -622,7 +628,7 @@ func Read(file util.File, size, start, sectorsize int64) (*FileSystem, error) {
 	// load the information from the disk
 	// read boot sector code
 	bs := make([]byte, BootSectorSize)
-	n, err := file.ReadAt(bs, start)
+	n, err := b.ReadAt(bs, start)
 	if err != nil {
 		return nil, fmt.Errorf("could not read boot sector bytes from file: %v", err)
 	}
@@ -633,7 +639,7 @@ func Read(file util.File, size, start, sectorsize int64) (*FileSystem, error) {
 	// read the superblock
 	// the superblock is one minimal block, i.e. 2 sectors
 	superblockBytes := make([]byte, SuperblockSize)
-	n, err = file.ReadAt(superblockBytes, start+int64(BootSectorSize))
+	n, err = b.ReadAt(superblockBytes, start+int64(BootSectorSize))
 	if err != nil {
 		return nil, fmt.Errorf("could not read superblock bytes from file: %v", err)
 	}
@@ -661,7 +667,7 @@ func Read(file util.File, size, start, sectorsize int64) (*FileSystem, error) {
 	if sb.blockSize == 1024 {
 		gdtBlock = 2
 	}
-	n, err = file.ReadAt(gdtBytes, start+int64(gdtBlock)*int64(sb.blockSize))
+	n, err = b.ReadAt(gdtBytes, start+int64(gdtBlock)*int64(sb.blockSize))
 	if err != nil {
 		return nil, fmt.Errorf("could not read Group Descriptor Table bytes from file: %v", err)
 	}
@@ -680,7 +686,7 @@ func Read(file util.File, size, start, sectorsize int64) (*FileSystem, error) {
 		blockGroups:      int64(sb.blockGroupCount()),
 		size:             size,
 		start:            start,
-		file:             file,
+		backend:          b,
 	}, nil
 }
 
@@ -877,6 +883,12 @@ func (fs *FileSystem) Remove(p string) error {
 	if entry == nil {
 		return fmt.Errorf("file does not exist: %s", p)
 	}
+
+	writableFile, err := fs.backend.Writable()
+
+	if err != nil {
+		return err
+	}
 	// if it is a directory, it must be empty
 	if entry.fileType == dirFileTypeDirectory {
 		// read the directory
@@ -956,7 +968,7 @@ func (fs *FileSystem) Remove(p string) error {
 	for _, e := range extents {
 		for i := 0; i < int(e.count); i++ {
 			b := dirBytes[i:fs.superblock.blockSize]
-			if _, err := fs.file.WriteAt(b, (int64(i)+int64(e.startingBlock))*int64(fs.superblock.blockSize)); err != nil {
+			if _, err := writableFile.WriteAt(b, (int64(i)+int64(e.startingBlock))*int64(fs.superblock.blockSize)); err != nil {
 				return fmt.Errorf("could not write inode bitmap back to disk: %v", err)
 			}
 		}
@@ -985,7 +997,7 @@ func (fs *FileSystem) Remove(p string) error {
 	if fs.superblock.blockSize == 1024 {
 		gdtBlock = 2
 	}
-	if _, err := fs.file.WriteAt(gdBytes, fs.start+int64(gdtBlock)*int64(fs.superblock.blockSize)+int64(gd.number)*int64(fs.superblock.groupDescriptorSize)); err != nil {
+	if _, err := writableFile.WriteAt(gdBytes, fs.start+int64(gdtBlock)*int64(fs.superblock.blockSize)+int64(gd.number)*int64(fs.superblock.groupDescriptorSize)); err != nil {
 		return fmt.Errorf("could not write Group Descriptor bytes to file: %v", err)
 	}
 
@@ -1100,7 +1112,7 @@ func (fs *FileSystem) readInode(inodeNumber uint32) (*inode, error) {
 	offsetInode := (inodeNumber - 1) % inodesPerGroup
 	// offset is how many bytes in our inode is
 	offset := offsetInode * uint32(inodeSize)
-	read, err := fs.file.ReadAt(inodeBytes, int64(byteStart)+int64(offset))
+	read, err := fs.backend.ReadAt(inodeBytes, int64(byteStart)+int64(offset))
 	if err != nil {
 		return nil, fmt.Errorf("failed to read inode %d from offset %d of block %d from block group %d: %v", inodeNumber, offset, inodeTableBlock, bg, err)
 	}
@@ -1129,6 +1141,12 @@ func (fs *FileSystem) readInode(inodeNumber uint32) (*inode, error) {
 
 // writeInode write a single inode to disk
 func (fs *FileSystem) writeInode(i *inode) error {
+	writableFile, err := fs.backend.Writable()
+
+	if err != nil {
+		return err
+	}
+
 	sb := fs.superblock
 	inodeSize := sb.inodeSize
 	inodesPerGroup := sb.inodesPerGroup
@@ -1147,7 +1165,7 @@ func (fs *FileSystem) writeInode(i *inode) error {
 	// offset is how many bytes in our inode is
 	offset := int64(offsetInode) * int64(inodeSize)
 	inodeBytes := i.toBytes(sb)
-	wrote, err := fs.file.WriteAt(inodeBytes, int64(byteStart)+offset)
+	wrote, err := writableFile.WriteAt(inodeBytes, int64(byteStart)+offset)
 	if err != nil {
 		return fmt.Errorf("failed to write inode %d at offset %d of block %d from block group %d: %v", i.number, offset, inodeTableBlock, bg, err)
 	}
@@ -1209,7 +1227,7 @@ func (fs *FileSystem) readFileBytes(extents extents, filesize uint64) ([]byte, e
 			count = filesize - uint64(len(b))
 		}
 		b2 := make([]byte, count)
-		read, err := fs.file.ReadAt(b2, int64(start))
+		read, err := fs.backend.ReadAt(b2, int64(start))
 		if err != nil {
 			return nil, fmt.Errorf("failed to read bytes for extent %d: %v", i, err)
 		}
@@ -1305,7 +1323,7 @@ func (fs *FileSystem) readBlock(blockNumber uint64) ([]byte, error) {
 	// bytesStart is beginning byte for the inodeTableBlock
 	byteStart := blockNumber * uint64(sb.blockSize)
 	blockBytes := make([]byte, sb.blockSize)
-	read, err := fs.file.ReadAt(blockBytes, int64(byteStart))
+	read, err := fs.backend.ReadAt(blockBytes, int64(byteStart))
 	if err != nil {
 		return nil, fmt.Errorf("failed to read block %d: %v", blockNumber, err)
 	}
@@ -1528,6 +1546,11 @@ func (fs *FileSystem) allocateInode(parent uint32) (uint32, error) {
 		gd groupDescriptor
 	)
 
+	writableFile, err := fs.backend.Writable()
+	if err != nil {
+		return 0, err
+	}
+
 	for _, gd = range fs.groupDescriptors.descriptors {
 		if inodeNumber != -1 {
 			break
@@ -1567,7 +1590,7 @@ func (fs *FileSystem) allocateInode(parent uint32) (uint32, error) {
 	gdtBlock := 1
 	blockByteLocation := gdtBlock * int(fs.superblock.blockSize)
 	gdOffset := fs.start + int64(blockByteLocation) + int64(bg)*int64(fs.superblock.groupDescriptorSize)
-	wrote, err := fs.file.WriteAt(gdBytes, gdOffset)
+	wrote, err := writableFile.WriteAt(gdBytes, gdOffset)
 	if err != nil {
 		return 0, fmt.Errorf("unable to write group descriptor bytes for blockgroup %d: %v", bg, err)
 	}
@@ -1719,7 +1742,7 @@ func (fs *FileSystem) readInodeBitmap(group int) (*util.Bitmap, error) {
 	bitmapByteCount := fs.superblock.inodesPerGroup / 8
 	b := make([]byte, bitmapByteCount)
 	offset := int64(bitmapLocation*uint64(fs.superblock.blockSize) + uint64(fs.start))
-	read, err := fs.file.ReadAt(b, offset)
+	read, err := fs.backend.ReadAt(b, offset)
 	if err != nil {
 		return nil, fmt.Errorf("unable to read inode bitmap for blockgroup %d: %w", gd.number, err)
 	}
@@ -1739,12 +1762,16 @@ func (fs *FileSystem) writeInodeBitmap(bm *util.Bitmap, group int) error {
 	if group >= len(fs.groupDescriptors.descriptors) {
 		return fmt.Errorf("block group %d does not exist", group)
 	}
+	writableFile, err := fs.backend.Writable()
+	if err != nil {
+		return err
+	}
 	b := bm.ToBytes()
 	gd := fs.groupDescriptors.descriptors[group]
 	bitmapByteCount := fs.superblock.inodesPerGroup / 8
 	bitmapLocation := gd.inodeBitmapLocation
 	offset := int64(bitmapLocation*uint64(fs.superblock.blockSize) + uint64(fs.start))
-	wrote, err := fs.file.WriteAt(b, offset)
+	wrote, err := writableFile.WriteAt(b, offset)
 	if err != nil {
 		return fmt.Errorf("unable to write inode bitmap for blockgroup %d: %w", gd.number, err)
 	}
@@ -1763,7 +1790,7 @@ func (fs *FileSystem) readBlockBitmap(group int) (*util.Bitmap, error) {
 	bitmapLocation := gd.blockBitmapLocation
 	b := make([]byte, fs.superblock.blockSize)
 	offset := int64(bitmapLocation*uint64(fs.superblock.blockSize) + uint64(fs.start))
-	read, err := fs.file.ReadAt(b, offset)
+	read, err := fs.backend.ReadAt(b, offset)
 	if err != nil {
 		return nil, fmt.Errorf("unable to read block bitmap for blockgroup %d: %w", gd.number, err)
 	}
@@ -1781,11 +1808,15 @@ func (fs *FileSystem) writeBlockBitmap(bm *util.Bitmap, group int) error {
 	if group >= len(fs.groupDescriptors.descriptors) {
 		return fmt.Errorf("block group %d does not exist", group)
 	}
+	writableFile, err := fs.backend.Writable()
+	if err != nil {
+		return err
+	}
 	b := bm.ToBytes()
 	gd := fs.groupDescriptors.descriptors[group]
 	bitmapLocation := gd.blockBitmapLocation
 	offset := int64(bitmapLocation*uint64(fs.superblock.blockSize) + uint64(fs.start))
-	wrote, err := fs.file.WriteAt(b, offset)
+	wrote, err := writableFile.WriteAt(b, offset)
 	if err != nil {
 		return fmt.Errorf("unable to write block bitmap for blockgroup %d: %w", gd.number, err)
 	}
@@ -1797,11 +1828,15 @@ func (fs *FileSystem) writeBlockBitmap(bm *util.Bitmap, group int) error {
 }
 
 func (fs *FileSystem) writeSuperblock() error {
+	writableFile, err := fs.backend.Writable()
+	if err != nil {
+		return err
+	}
 	superblockBytes, err := fs.superblock.toBytes()
 	if err != nil {
 		return fmt.Errorf("could not convert superblock to bytes: %v", err)
 	}
-	_, err = fs.file.WriteAt(superblockBytes, fs.start+int64(BootSectorSize))
+	_, err = writableFile.WriteAt(superblockBytes, fs.start+int64(BootSectorSize))
 	return err
 }
 
diff --git a/filesystem/ext4/extent.go b/filesystem/ext4/extent.go
index e5d456e..c107c4c 100644
--- a/filesystem/ext4/extent.go
+++ b/filesystem/ext4/extent.go
@@ -680,8 +680,13 @@ func writeNodeToDisk(node extentBlockFinder, fs *FileSystem, parent *extentInter
 		return fmt.Errorf("block number not found for node")
 	}
 
+	writableFile, err := fs.backend.Writable()
+	if err != nil {
+		return err
+	}
+
 	data := node.toBytes()
-	_, err := fs.file.WriteAt(data, int64(blockNumber)*int64(fs.superblock.blockSize))
+	_, err = writableFile.WriteAt(data, int64(blockNumber)*int64(fs.superblock.blockSize))
 	return err
 }
 
@@ -720,7 +725,7 @@ func findChildNode(node *extentInternalNode, added *extents) int {
 //nolint:unparam // this parameter will be used eventually
 func loadChildNode(childPtr *extentChildPtr, fs *FileSystem) (extentBlockFinder, error) {
 	data := make([]byte, fs.superblock.blockSize)
-	_, err := fs.file.ReadAt(data, int64(childPtr.diskBlock)*int64(fs.superblock.blockSize))
+	_, err := fs.backend.ReadAt(data, int64(childPtr.diskBlock)*int64(fs.superblock.blockSize))
 	if err != nil {
 		return nil, err
 	}
diff --git a/filesystem/ext4/file.go b/filesystem/ext4/file.go
index 4dc6539..733a6ac 100644
--- a/filesystem/ext4/file.go
+++ b/filesystem/ext4/file.go
@@ -61,7 +61,7 @@ func (fl *File) Read(b []byte) (int, error) {
 		// read those bytes
 		startPosOnDisk := e.startingBlock*blocksize + uint64(startPositionInExtent)
 		b2 := make([]byte, toReadInOffset)
-		read, err := fl.filesystem.file.ReadAt(b2, int64(startPosOnDisk))
+		read, err := fl.filesystem.backend.ReadAt(b2, int64(startPosOnDisk))
 		if err != nil {
 			return int(readBytes), fmt.Errorf("failed to read bytes: %v", err)
 		}
@@ -145,6 +145,12 @@ func (fl *File) Write(b []byte) (int, error) {
 	// the offset given for reading is relative to the file, so we need to calculate
 	// where these are in the extents relative to the file
 	writeStartBlock := uint64(fl.offset) / blocksize
+
+	writableFile, err := fl.filesystem.backend.Writable()
+	if err != nil {
+		return -1, err
+	}
+
 	for _, e := range fl.extents {
 		// if the last block of the extent is before the first block we want to write, skip it
 		if uint64(e.fileBlock)+uint64(e.count) < writeStartBlock {
@@ -164,7 +170,7 @@ func (fl *File) Write(b []byte) (int, error) {
 		startPosOnDisk := e.startingBlock*blocksize + uint64(startPositionInExtent)
 		b2 := make([]byte, toWriteInOffset)
 		copy(b2, b[writtenBytes:])
-		written, err := fl.filesystem.file.WriteAt(b2, int64(startPosOnDisk))
+		written, err := writableFile.WriteAt(b2, int64(startPosOnDisk))
 		if err != nil {
 			return int(writtenBytes), fmt.Errorf("failed to read bytes: %v", err)
 		}
@@ -175,7 +181,7 @@ func (fl *File) Write(b []byte) (int, error) {
 			break
 		}
 	}
-	var err error
+
 	if fl.offset >= fileSize {
 		err = io.EOF
 	}
diff --git a/filesystem/fat32/fat32.go b/filesystem/fat32/fat32.go
index 0d73567..10db7fc 100644
--- a/filesystem/fat32/fat32.go
+++ b/filesystem/fat32/fat32.go
@@ -8,8 +8,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/filesystem"
-	"github.com/diskfs/go-diskfs/util"
 )
 
 // MsdosMediaType is the (mostly unused) media type. However, we provide and export the known constants for it.
@@ -59,7 +59,7 @@ type FileSystem struct {
 	bytesPerCluster int
 	size            int64
 	start           int64
-	file            util.File
+	backend         backend.Storage
 }
 
 // Equal compare if two filesystems are equal
@@ -70,7 +70,7 @@ func (fs *FileSystem) Equal(a *FileSystem) bool {
 	if fs == nil || a == nil {
 		return false
 	}
-	localMatch := fs.file == a.file && fs.dataStart == a.dataStart && fs.bytesPerCluster == a.bytesPerCluster
+	localMatch := fs.backend == a.backend && fs.dataStart == a.dataStart && fs.bytesPerCluster == a.bytesPerCluster
 	tableMatch := fs.table.equal(&a.table)
 	bsMatch := fs.bootSector.equal(&a.bootSector)
 	fsisMatch := fs.fsis == a.fsis
@@ -79,8 +79,8 @@ func (fs *FileSystem) Equal(a *FileSystem) bool {
 
 // Create creates a FAT32 filesystem in a given file or device
 //
-// requires the util.File where to create the filesystem, size is the size of the filesystem in bytes,
-// start is how far in bytes from the beginning of the util.File to create the filesystem,
+// requires the backend.Storage where to create the filesystem, size is the size of the filesystem in bytes,
+// start is how far in bytes from the beginning of the backend.Storage to create the filesystem,
 // and blocksize is is the logical blocksize to use for creating the filesystem
 //
 // note that you are *not* required to create the filesystem on the entire disk. You could have a disk of size
@@ -93,7 +93,7 @@ func (fs *FileSystem) Equal(a *FileSystem) bool {
 //
 // If the provided blocksize is 0, it will use the default of 512 bytes. If it is any number other than 0
 // or 512, it will return an error.
-func Create(f util.File, size, start, blocksize int64, volumeLabel string) (*FileSystem, error) {
+func Create(b backend.Storage, size, start, blocksize int64, volumeLabel string) (*FileSystem, error) {
 	// blocksize must be <=0 or exactly SectorSize512 or error
 	if blocksize != int64(SectorSize512) && blocksize > 0 {
 		return nil, fmt.Errorf("blocksize for FAT32 must be either 512 bytes or 0, not %d", blocksize)
@@ -112,6 +112,11 @@ func Create(f util.File, size, start, blocksize int64, volumeLabel string) (*Fil
 	fsisPrimarySector := uint16(1)
 	backupBootSector := uint16(6)
 
+	writableFile, err := b.Writable()
+	if err != nil {
+		return nil, err
+	}
+
 	/*
 		size calculations
 		we have the total size of the disk from `size uint64`
@@ -252,7 +257,7 @@ func Create(f util.File, size, start, blocksize int64, volumeLabel string) (*Fil
 		bytesPerCluster: int(sectorsPerCluster) * int(SectorSize512),
 		start:           start,
 		size:            size,
-		file:            f,
+		backend:         b,
 	}
 
 	// write the boot sector
@@ -277,7 +282,7 @@ func Create(f util.File, size, start, blocksize int64, volumeLabel string) (*Fil
 	// length of cluster in bytes
 	tmpb := make([]byte, fs.bytesPerCluster)
 	// zero out the root directory cluster
-	written, err := f.WriteAt(tmpb, clusterStart)
+	written, err := writableFile.WriteAt(tmpb, clusterStart)
 	if err != nil {
 		return nil, fmt.Errorf("failed to zero out root directory: %w", err)
 	}
@@ -310,8 +315,8 @@ func Create(f util.File, size, start, blocksize int64, volumeLabel string) (*Fil
 
 // Read reads a filesystem from a given disk.
 //
-// requires the util.File where to read the filesystem, size is the size of the filesystem in bytes,
-// start is how far in bytes from the beginning of the util.File the filesystem is expected to begin,
+// requires the backend.Storage where to read the filesystem, size is the size of the filesystem in bytes,
+// start is how far in bytes from the beginning of the backend.Storage the filesystem is expected to begin,
 // and blocksize is is the logical blocksize to use for creating the filesystem
 //
 // note that you are *not* required to read a filesystem on the entire disk. You could have a disk of size
@@ -324,7 +329,7 @@ func Create(f util.File, size, start, blocksize int64, volumeLabel string) (*Fil
 //
 // If the provided blocksize is 0, it will use the default of 512 bytes. If it is any number other than 0
 // or 512, it will return an error.
-func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
+func Read(b backend.Storage, size, start, blocksize int64) (*FileSystem, error) {
 	// blocksize must be <=0 or exactly SectorSize512 or error
 	if blocksize != int64(SectorSize512) && blocksize > 0 {
 		return nil, fmt.Errorf("blocksize for FAT32 must be either 512 bytes or 0, not %d", blocksize)
@@ -335,11 +340,10 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 	if size < blocksize*4 {
 		return nil, fmt.Errorf("requested size is smaller than minimum allowed FAT32 size %d", blocksize*4)
 	}
-
 	// load the information from the disk
 	// read first 512 bytes from the file
 	bsb := make([]byte, SectorSize512)
-	n, err := file.ReadAt(bsb, start)
+	n, err := b.ReadAt(bsb, start)
 	if err != nil {
 		return nil, fmt.Errorf("could not read bytes from file: %w", err)
 	}
@@ -360,7 +364,7 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 	fatSecondaryStart := fatPrimaryStart + uint64(fatSize)
 
 	fsisBytes := make([]byte, 512)
-	read, err := file.ReadAt(fsisBytes, int64(bs.biosParameterBlock.fsInformationSector)*blocksize+start)
+	read, err := b.ReadAt(fsisBytes, int64(bs.biosParameterBlock.fsInformationSector)*blocksize+start)
 	if err != nil {
 		return nil, fmt.Errorf("unable to read bytes for FSInformationSector: %w", err)
 	}
@@ -372,12 +376,12 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 		return nil, fmt.Errorf("error reading FileSystem Information Sector: %w", err)
 	}
 
-	b := make([]byte, fatSize)
-	_, _ = file.ReadAt(b, int64(fatPrimaryStart)+start)
-	fat := tableFromBytes(b)
+	partitionTableBytes := make([]byte, fatSize)
+	_, _ = b.ReadAt(partitionTableBytes, int64(fatPrimaryStart)+start)
+	fat := tableFromBytes(partitionTableBytes)
 
-	_, _ = file.ReadAt(b, int64(fatSecondaryStart)+start)
-	fat2 := tableFromBytes(b)
+	_, _ = b.ReadAt(partitionTableBytes, int64(fatSecondaryStart)+start)
+	fat2 := tableFromBytes(partitionTableBytes)
 	if !fat.equal(fat2) {
 		return nil, errors.New("fat tables did not match")
 	}
@@ -391,7 +395,7 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 		bytesPerCluster: int(sectorsPerCluster) * int(SectorSize512),
 		start:           start,
 		size:            size,
-		file:            file,
+		backend:         b,
 	}, nil
 }
 
@@ -403,6 +407,10 @@ func (fs *FileSystem) writeBootSector() error {
 			return nil, fmt.Errorf("error writing MS-DOS Boot Sector: %v", err)
 		}
 	*/
+	writableFile, err := fs.backend.Writable()
+	if err != nil {
+		return err
+	}
 
 	b, err := fs.bootSector.toBytes()
 	if err != nil {
@@ -410,7 +418,7 @@ func (fs *FileSystem) writeBootSector() error {
 	}
 
 	// write main boot sector
-	count, err := fs.file.WriteAt(b, 0+fs.start)
+	count, err := writableFile.WriteAt(b, 0+fs.start)
 	if err != nil {
 		return fmt.Errorf("error writing MS-DOS Boot Sector to disk: %w", err)
 	}
@@ -420,7 +428,7 @@ func (fs *FileSystem) writeBootSector() error {
 
 	// write backup boot sector to the file
 	if fs.bootSector.biosParameterBlock.backupBootSector > 0 {
-		count, err = fs.file.WriteAt(b, int64(fs.bootSector.biosParameterBlock.backupBootSector)*int64(SectorSize512)+fs.start)
+		count, err = writableFile.WriteAt(b, int64(fs.bootSector.biosParameterBlock.backupBootSector)*int64(SectorSize512)+fs.start)
 		if err != nil {
 			return fmt.Errorf("error writing MS-DOS Boot Sector to disk: %w", err)
 		}
@@ -438,13 +446,17 @@ func (fs *FileSystem) writeFsis() error {
 	fsisPrimary := int64(fsInformationSector * uint16(SectorSize512))
 
 	fsisBytes := fs.fsis.toBytes()
+	writableFile, err := fs.backend.Writable()
+	if err != nil {
+		return err
+	}
 
-	if _, err := fs.file.WriteAt(fsisBytes, fsisPrimary+fs.start); err != nil {
+	if _, err := writableFile.WriteAt(fsisBytes, fsisPrimary+fs.start); err != nil {
 		return fmt.Errorf("unable to write primary Fsis: %w", err)
 	}
 
 	if backupBootSector > 0 {
-		if _, err := fs.file.WriteAt(fsisBytes, int64(backupBootSector+1)*int64(SectorSize512)+fs.start); err != nil {
+		if _, err := writableFile.WriteAt(fsisBytes, int64(backupBootSector+1)*int64(SectorSize512)+fs.start); err != nil {
 			return fmt.Errorf("unable to write backup Fsis: %w", err)
 		}
 	}
@@ -458,12 +470,16 @@ func (fs *FileSystem) writeFat() error {
 	fatSecondaryStart := fatPrimaryStart + uint64(fs.table.size)
 
 	fatBytes := fs.table.bytes()
+	writableFile, err := fs.backend.Writable()
+	if err != nil {
+		return err
+	}
 
-	if _, err := fs.file.WriteAt(fatBytes, int64(fatPrimaryStart)+fs.start); err != nil {
+	if _, err := writableFile.WriteAt(fatBytes, int64(fatPrimaryStart)+fs.start); err != nil {
 		return fmt.Errorf("unable to write primary FAT table: %w", err)
 	}
 
-	if _, err := fs.file.WriteAt(fatBytes, int64(fatSecondaryStart)+fs.start); err != nil {
+	if _, err := writableFile.WriteAt(fatBytes, int64(fatSecondaryStart)+fs.start); err != nil {
 		return fmt.Errorf("unable to write backup FAT table: %w", err)
 	}
 
@@ -889,7 +905,7 @@ func (fs *FileSystem) readDirectory(dir *Directory) ([]*directoryEntry, error) {
 		// length of cluster in bytes
 		tmpb := make([]byte, fs.bytesPerCluster)
 		// read the entire cluster
-		_, _ = fs.file.ReadAt(tmpb, clusterStart)
+		_, _ = fs.backend.ReadAt(tmpb, clusterStart)
 		b = append(b, tmpb...)
 	}
 	// get the directory
@@ -916,6 +932,11 @@ func (fs *FileSystem) writeDirectoryEntries(dir *Directory) error {
 	if err != nil {
 		return fmt.Errorf("could not create a valid byte stream for a FAT32 Entries: %w", err)
 	}
+
+	writableFile, err := fs.backend.Writable()
+	if err != nil {
+		return err
+	}
 	// now have to expand with zeros to the a multiple of cluster lengths
 	// how many clusters do we need, how many do we have?
 	clusterList, err := fs.getClusterList(dir.clusterLocation)
@@ -936,7 +957,7 @@ func (fs *FileSystem) writeDirectoryEntries(dir *Directory) error {
 		// bytes where the cluster starts
 		clusterStart := fs.start + int64(fs.dataStart) + int64(cluster-2)*int64(fs.bytesPerCluster)
 		bStart := i * fs.bytesPerCluster
-		written, err := fs.file.WriteAt(b[bStart:bStart+fs.bytesPerCluster], clusterStart)
+		written, err := writableFile.WriteAt(b[bStart:bStart+fs.bytesPerCluster], clusterStart)
 		if err != nil {
 			return fmt.Errorf("error writing directory entries: %w", err)
 		}
diff --git a/filesystem/fat32/file.go b/filesystem/fat32/file.go
index d6cdb79..043d776 100644
--- a/filesystem/fat32/file.go
+++ b/filesystem/fat32/file.go
@@ -37,7 +37,7 @@ func (fl *File) Read(b []byte) (int, error) {
 	start := int(fs.dataStart)
 	size := int(fl.fileSize) - int(fl.offset)
 	maxRead := size
-	file := fs.file
+	file := fs.backend
 	clusters, err := fs.getClusterList(fl.clusterLocation)
 	if err != nil {
 		return totalRead, fmt.Errorf("unable to get list of clusters for file: %v", err)
@@ -106,7 +106,13 @@ func (fl *File) Write(p []byte) (int, error) {
 	if fl == nil || fl.filesystem == nil {
 		return 0, os.ErrClosed
 	}
+
 	totalWritten := 0
+	writableFile, err := fl.filesystem.backend.Writable()
+	if err != nil {
+		return totalWritten, err
+	}
+
 	fs := fl.filesystem
 	// if the file was not opened RDWR, nothing we can do
 	if !fl.isReadWrite {
@@ -131,7 +137,6 @@ func (fl *File) Write(p []byte) (int, error) {
 	}
 	// write the content for the file
 	bytesPerCluster := fl.filesystem.bytesPerCluster
-	file := fl.filesystem.file
 	start := int(fl.filesystem.dataStart)
 	clusterIndex := 0
 
@@ -148,7 +153,7 @@ func (fl *File) Write(p []byte) (int, error) {
 			if toWrite > int64(len(p)) {
 				toWrite = int64(len(p))
 			}
-			_, err := file.WriteAt(p[0:toWrite], offset+fs.start)
+			_, err := writableFile.WriteAt(p[0:toWrite], offset+fs.start)
 			if err != nil {
 				return totalWritten, fmt.Errorf("unable to write to file: %v", err)
 			}
@@ -164,7 +169,7 @@ func (fl *File) Write(p []byte) (int, error) {
 			toWrite = left
 		}
 		offset := int64(start) + int64(clusters[i]-2)*int64(bytesPerCluster)
-		_, err := file.WriteAt(p[totalWritten:totalWritten+toWrite], offset+fs.start)
+		_, err := writableFile.WriteAt(p[totalWritten:totalWritten+toWrite], offset+fs.start)
 		if err != nil {
 			return totalWritten, fmt.Errorf("unable to write to file: %v", err)
 		}
diff --git a/filesystem/iso9660/directoryentry.go b/filesystem/iso9660/directoryentry.go
index d987767..d76b78f 100644
--- a/filesystem/iso9660/directoryentry.go
+++ b/filesystem/iso9660/directoryentry.go
@@ -305,7 +305,7 @@ func parseDirEntry(b []byte, f *FileSystem) (*directoryEntry, error) {
 				offset := int64(ce.Offset())
 				// read it from disk
 				continuationBytes := make([]byte, size)
-				read, err := f.file.ReadAt(continuationBytes, location*f.blocksize+offset)
+				read, err := f.backend.ReadAt(continuationBytes, location*f.blocksize+offset)
 				if err != nil {
 					return nil, fmt.Errorf("error reading continuation entry data at %d: %v", location, err)
 				}
@@ -373,7 +373,7 @@ func (de *directoryEntry) getLocation(p string) (location, size uint32, err erro
 		current := parts[0]
 		// read the directory bytes
 		dirb := make([]byte, de.size)
-		n, err := de.filesystem.file.ReadAt(dirb, int64(de.location)*de.filesystem.blocksize)
+		n, err := de.filesystem.backend.ReadAt(dirb, int64(de.location)*de.filesystem.blocksize)
 		if err != nil {
 			return 0, 0, fmt.Errorf("could not read directory: %v", err)
 		}
@@ -414,7 +414,7 @@ func (de *directoryEntry) getLocation(p string) (location, size uint32, err erro
 							if location2 != 0 {
 								// need to get the directory entry for the child
 								dirb := make([]byte, de.filesystem.blocksize)
-								n, err2 := de.filesystem.file.ReadAt(dirb, int64(location2)*de.filesystem.blocksize)
+								n, err2 := de.filesystem.backend.ReadAt(dirb, int64(location2)*de.filesystem.blocksize)
 								if err2 != nil {
 									return 0, 0, fmt.Errorf("could not read bytes of relocated directory %s from block %d: %v", checkFilename, location2, err2)
 								}
diff --git a/filesystem/iso9660/file.go b/filesystem/iso9660/file.go
index 0b10ff8..a00acf9 100644
--- a/filesystem/iso9660/file.go
+++ b/filesystem/iso9660/file.go
@@ -35,7 +35,7 @@ func (fl *File) Read(b []byte) (int, error) {
 	size := int(fl.size) - int(fl.offset)
 	location := int(fl.location)
 	maxRead := size
-	file := fs.file
+	file := fs.backend
 
 	// if there is nothing left to read, just return EOF
 	if size <= 0 {
diff --git a/filesystem/iso9660/finalize.go b/filesystem/iso9660/finalize.go
index aaa3262..cc2e2b1 100644
--- a/filesystem/iso9660/finalize.go
+++ b/filesystem/iso9660/finalize.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/util"
 	"github.com/djherbis/times"
 )
@@ -463,7 +464,11 @@ func (fsm *FileSystem) Finalize(options FinalizeOptions) error {
 		 10- write volume descriptor set terminator
 	*/
 
-	f := fsm.file
+	f, err := fsm.backend.Writable()
+	if err != nil {
+		return err
+	}
+
 	blocksize := int(fsm.blocksize)
 
 	// 1- blank out sectors 0-15
@@ -816,7 +821,7 @@ func (fsm *FileSystem) Finalize(options FinalizeOptions) error {
 
 // copyFileData copy data from file `from` at offset `fromOffset` to file `to` at offset `toOffset`.
 // Copies `size` bytes. If `size` is 0, copies as many bytes as it can.
-func copyFileData(from, to util.File, fromOffset, toOffset int64, size int) (int, error) {
+func copyFileData(from backend.File, to backend.WritableFile, fromOffset, toOffset int64, size int) (int, error) {
 	buf := make([]byte, 2048)
 	copied := 0
 	for {
diff --git a/filesystem/iso9660/iso9660.go b/filesystem/iso9660/iso9660.go
index 6cb56df..fcfda3b 100644
--- a/filesystem/iso9660/iso9660.go
+++ b/filesystem/iso9660/iso9660.go
@@ -6,8 +6,8 @@ import (
 	"os"
 	"path"
 
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/filesystem"
-	"github.com/diskfs/go-diskfs/util"
 )
 
 const (
@@ -23,7 +23,7 @@ type FileSystem struct {
 	workspace      string
 	size           int64
 	start          int64
-	file           util.File
+	backend        backend.Storage
 	blocksize      int64
 	volumes        volumeDescriptors
 	pathTable      *pathTable
@@ -35,7 +35,7 @@ type FileSystem struct {
 
 // Equal compare if two filesystems are equal
 func (fsm *FileSystem) Equal(a *FileSystem) bool {
-	localMatch := fsm.file == a.file && fsm.size == a.size
+	localMatch := fsm.backend == a.backend && fsm.size == a.size
 	vdMatch := fsm.volumes.equal(&a.volumes)
 	return localMatch && vdMatch
 }
@@ -47,8 +47,8 @@ func (fsm *FileSystem) Workspace() string {
 
 // Create creates an ISO9660 filesystem in a given directory
 //
-// requires the util.File where to create the filesystem, size is the size of the filesystem in bytes,
-// start is how far in bytes from the beginning of the util.File to create the filesystem,
+// requires the backend.Storage where to create the filesystem, size is the size of the filesystem in bytes,
+// start is how far in bytes from the beginning of the backend.Storage to create the filesystem,
 // and blocksize is is the logical blocksize to use for creating the filesystem
 //
 // note that you are *not* required to create the filesystem on the entire disk. You could have a disk of size
@@ -60,7 +60,7 @@ func (fsm *FileSystem) Workspace() string {
 // where a partition starts and ends.
 //
 // If the provided blocksize is 0, it will use the default of 2 KB.
-func Create(f util.File, size, start, blocksize int64, workspace string) (*FileSystem, error) {
+func Create(b backend.Storage, size, start, blocksize int64, workspace string) (*FileSystem, error) {
 	if blocksize == 0 {
 		blocksize = defaultSectorSize
 	}
@@ -103,7 +103,7 @@ func Create(f util.File, size, start, blocksize int64, workspace string) (*FileS
 		workspace: workdir,
 		start:     start,
 		size:      size,
-		file:      f,
+		backend:   b,
 		volumes:   volumeDescriptors{},
 		blocksize: blocksize,
 	}, nil
@@ -111,8 +111,8 @@ func Create(f util.File, size, start, blocksize int64, workspace string) (*FileS
 
 // Read reads a filesystem from a given disk.
 //
-// requires the util.File where to read the filesystem, size is the size of the filesystem in bytes,
-// start is how far in bytes from the beginning of the util.File the filesystem is expected to begin,
+// requires the backend.File where to read the filesystem, size is the size of the filesystem in bytes,
+// start is how far in bytes from the beginning of the backend.File the filesystem is expected to begin,
 // and blocksize is is the physical blocksize to use for reading the filesystem
 //
 // note that you are *not* required to read a filesystem on the entire disk. You could have a disk of size
@@ -124,7 +124,7 @@ func Create(f util.File, size, start, blocksize int64, workspace string) (*FileS
 // where a partition starts and ends.
 //
 // If the provided blocksize is 0, it will use the default of 2K bytes
-func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
+func Read(b backend.Storage, size, start, blocksize int64) (*FileSystem, error) {
 	var read int
 
 	if blocksize == 0 {
@@ -146,7 +146,7 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 	// load the information from the disk
 	// read system area
 	systemArea := make([]byte, systemAreaSize)
-	n, err := file.ReadAt(systemArea, start)
+	n, err := b.ReadAt(systemArea, start)
 	if err != nil {
 		return nil, fmt.Errorf("could not read bytes from file: %v", err)
 	}
@@ -165,7 +165,7 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 	for i := 0; !terminated; i++ {
 		vdBytes := make([]byte, volumeDescriptorSize)
 		// read vdBytes
-		read, err = file.ReadAt(vdBytes, start+systemAreaSize+int64(i)*volumeDescriptorSize)
+		read, err = b.ReadAt(vdBytes, start+systemAreaSize+int64(i)*volumeDescriptorSize)
 		if err != nil {
 			return nil, fmt.Errorf("unable to read bytes for volume descriptor %d: %v", i, err)
 		}
@@ -199,7 +199,7 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 		rootDirEntry = pvd.rootDirectoryEntry
 		pathTableBytes := make([]byte, pvd.pathTableSize)
 		pathTableLocation := pvd.pathTableLLocation * uint32(pvd.blocksize)
-		read, err = file.ReadAt(pathTableBytes, int64(pathTableLocation))
+		read, err = b.ReadAt(pathTableBytes, int64(pathTableLocation))
 		if err != nil {
 			return nil, fmt.Errorf("unable to read path table of size %d at location %d: %v", pvd.pathTableSize, pathTableLocation, err)
 		}
@@ -212,30 +212,30 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 	// is system use enabled?
 	location := int64(rootDirEntry.location) * blocksize
 	// get the size of the directory entry
-	b := make([]byte, 1)
-	read, err = file.ReadAt(b, location)
+	dirEntBytes := make([]byte, 1)
+	read, err = b.ReadAt(dirEntBytes, location)
 	if err != nil {
 		return nil, fmt.Errorf("unable to read root directory size at location %d: %v", location, err)
 	}
-	if read != len(b) {
-		return nil, fmt.Errorf("root directory entry size, read %d bytes instead of expected %d", read, len(b))
+	if read != len(dirEntBytes) {
+		return nil, fmt.Errorf("root directory entry size, read %d bytes instead of expected %d", read, len(dirEntBytes))
 	}
-	if b[0] == 0 {
+	if dirEntBytes[0] == 0 {
 		return nil, fmt.Errorf("root directory entry size at location %d was zero, check header and blocksize, given as %d", location, blocksize)
 	}
 	// now read the whole entry
-	b = make([]byte, b[0])
-	read, err = file.ReadAt(b, location)
+	dirEntBytes = make([]byte, dirEntBytes[0])
+	read, err = b.ReadAt(dirEntBytes, location)
 	if err != nil {
 		return nil, fmt.Errorf("unable to read root directory entry at location %d: %v", location, err)
 	}
-	if read != len(b) {
-		return nil, fmt.Errorf("root directory entry, read %d bytes instead of expected %d", read, len(b))
+	if read != len(dirEntBytes) {
+		return nil, fmt.Errorf("root directory entry, read %d bytes instead of expected %d", read, len(dirEntBytes))
 	}
 	// parse it - we do not have any handlers yet
-	de, err := parseDirEntry(b, &FileSystem{
+	de, err := parseDirEntry(dirEntBytes, &FileSystem{
 		suspEnabled: true,
-		file:        file,
+		backend:     b,
 		blocksize:   blocksize,
 	})
 	if err != nil {
@@ -266,7 +266,7 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 		workspace: "", // no workspace when we do nothing with it
 		start:     start,
 		size:      size,
-		file:      file,
+		backend:   b,
 		volumes: volumeDescriptors{
 			descriptors: vds,
 			primary:     pvd,
@@ -501,7 +501,7 @@ func (fsm *FileSystem) readDirectory(p string) ([]*directoryEntry, error) {
 	if location != 0 {
 		// we need 4 bytes to read the size of the directory; it is at offset 10 from beginning
 		dirb := make([]byte, 4)
-		n, err = fsm.file.ReadAt(dirb, int64(location)*fsm.blocksize+10)
+		n, err = fsm.backend.ReadAt(dirb, int64(location)*fsm.blocksize+10)
 		if err != nil {
 			return nil, fmt.Errorf("could not read directory %s: %v", p, err)
 		}
@@ -526,7 +526,7 @@ func (fsm *FileSystem) readDirectory(p string) ([]*directoryEntry, error) {
 
 	// we have a location, let's read the directories from it
 	b := make([]byte, size)
-	n, err = fsm.file.ReadAt(b, int64(location)*fsm.blocksize)
+	n, err = fsm.backend.ReadAt(b, int64(location)*fsm.blocksize)
 	if err != nil {
 		return nil, fmt.Errorf("could not read directory entries for %s: %v", p, err)
 	}
diff --git a/filesystem/squashfs/finalize.go b/filesystem/squashfs/finalize.go
index 7c99e4f..1993fea 100644
--- a/filesystem/squashfs/finalize.go
+++ b/filesystem/squashfs/finalize.go
@@ -11,7 +11,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/diskfs/go-diskfs/util"
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/pkg/xattr"
 )
 
@@ -86,7 +86,11 @@ func (fs *FileSystem) Finalize(options FinalizeOptions) error {
 
 	*/
 
-	f := fs.file
+	f, err := fs.backend.Writable()
+	if err != nil {
+		return err
+	}
+
 	blocksize := int(fs.blocksize)
 	comp := compressionNone
 	if options.Compression != nil {
@@ -356,7 +360,7 @@ func (fs *FileSystem) Finalize(options FinalizeOptions) error {
 	return nil
 }
 
-func copyFileData(from, to util.File, fromOffset, toOffset, blocksize int64, c Compressor) (raw, compressed int, blocks []*blockData, err error) {
+func copyFileData(from backend.File, to backend.WritableFile, fromOffset, toOffset, blocksize int64, c Compressor) (raw, compressed int, blocks []*blockData, err error) {
 	buf := make([]byte, blocksize)
 	blocks = make([]*blockData, 0)
 	for {
@@ -392,7 +396,7 @@ func copyFileData(from, to util.File, fromOffset, toOffset, blocksize int64, c C
 
 // finalizeFragment write fragment data out to the archive, compressing if relevant.
 // Returns the total amount written, whether compressed, and any error.
-func finalizeFragment(buf []byte, to util.File, toOffset int64, c Compressor) (raw int, compressed bool, err error) {
+func finalizeFragment(buf []byte, to backend.WritableFile, toOffset int64, c Compressor) (raw int, compressed bool, err error) {
 	// compress the block if needed
 	if c != nil {
 		out, err := c.compress(buf)
@@ -517,7 +521,7 @@ func getTableIdx(m map[uint32]uint16, index uint32) uint16 {
 	return m[index]
 }
 
-func writeFileDataBlocks(e *finalizeFileInfo, to util.File, ws string, startBlock uint64, blocksize int, compressor Compressor, location int64) (blockCount, compressed int, err error) {
+func writeFileDataBlocks(e *finalizeFileInfo, to backend.WritableFile, ws string, startBlock uint64, blocksize int, compressor Compressor, location int64) (blockCount, compressed int, err error) {
 	from, err := os.Open(path.Join(ws, e.path))
 	if err != nil {
 		return 0, 0, fmt.Errorf("failed to open file for reading %s: %v", e.path, err)
@@ -541,7 +545,7 @@ func writeFileDataBlocks(e *finalizeFileInfo, to util.File, ws string, startBloc
 	return blockCount, compressed, nil
 }
 
-func writeMetadataBlock(buf []byte, to util.File, c Compressor, location int64) (int, error) {
+func writeMetadataBlock(buf []byte, to backend.WritableFile, c Compressor, location int64) (int, error) {
 	// compress the block if needed
 	isCompressed := false
 	if c != nil {
@@ -569,7 +573,7 @@ func writeMetadataBlock(buf []byte, to util.File, c Compressor, location int64)
 	return len(buf), nil
 }
 
-func writeDataBlocks(fileList []*finalizeFileInfo, f util.File, ws string, blocksize int, compressor Compressor, location int64) (int, error) {
+func writeDataBlocks(fileList []*finalizeFileInfo, f backend.WritableFile, ws string, blocksize int, compressor Compressor, location int64) (int, error) {
 	allBlocks := 0
 	allWritten := 0
 	for _, e := range fileList {
@@ -589,7 +593,7 @@ func writeDataBlocks(fileList []*finalizeFileInfo, f util.File, ws string, block
 }
 
 // writeFragmentBlocks writes all of the fragment blocks to the archive. Returns slice of blocks written, the total bytes written, any error
-func writeFragmentBlocks(fileList []*finalizeFileInfo, f util.File, ws string, blocksize int, options FinalizeOptions, location int64) ([]fragmentBlock, int64, error) {
+func writeFragmentBlocks(fileList []*finalizeFileInfo, f backend.WritableFile, ws string, blocksize int, options FinalizeOptions, location int64) ([]fragmentBlock, int64, error) {
 	compressor := options.Compression
 	if options.NoCompressFragments {
 		compressor = nil
@@ -683,7 +687,7 @@ func writeFragmentBlocks(fileList []*finalizeFileInfo, f util.File, ws string, b
 	return fragmentBlocks, allWritten, nil
 }
 
-func writeInodes(files []*finalizeFileInfo, f util.File, compressor Compressor, location int64) (inodesWritten int, finalLocation uint64, err error) {
+func writeInodes(files []*finalizeFileInfo, f backend.WritableFile, compressor Compressor, location int64) (inodesWritten int, finalLocation uint64, err error) {
 	var (
 		buf             []byte
 		maxSize         = int(metadataBlockSize)
@@ -717,7 +721,7 @@ func writeInodes(files []*finalizeFileInfo, f util.File, compressor Compressor,
 }
 
 // writeDirectories write all directories out to disk. Assumes it already has been optimized.
-func writeDirectories(dirs []*finalizeFileInfo, f util.File, compressor Compressor, location int64) (directoriesWritten int, finalLocation uint64, err error) {
+func writeDirectories(dirs []*finalizeFileInfo, f backend.WritableFile, compressor Compressor, location int64) (directoriesWritten int, finalLocation uint64, err error) {
 	var (
 		buf             []byte
 		maxSize         = int(metadataBlockSize)
@@ -756,7 +760,7 @@ func writeDirectories(dirs []*finalizeFileInfo, f util.File, compressor Compress
 // writeFragmentTable write the fragment table
 //
 //nolint:unparam,unused,revive // this does not use fragmentBlocksStart yet, but only because we have not yet added support
-func writeFragmentTable(fragmentBlocks []fragmentBlock, fragmentBlocksStart int64, f util.File, compressor Compressor, location int64) (fragmentsWritten int, finalLocation uint64, err error) {
+func writeFragmentTable(fragmentBlocks []fragmentBlock, fragmentBlocksStart int64, f backend.WritableFile, compressor Compressor, location int64) (fragmentsWritten int, finalLocation uint64, err error) {
 	// now write the actual fragment table entries
 	var (
 		indexEntries []uint64
@@ -818,7 +822,7 @@ func writeFragmentTable(fragmentBlocks []fragmentBlock, fragmentBlocksStart int6
 }
 
 // writeExportTable write the export table at the given location.
-func writeExportTable(files []*finalizeFileInfo, f util.File, compressor Compressor, location int64) (entriesWritten int, finalLocation uint64, err error) {
+func writeExportTable(files []*finalizeFileInfo, f backend.WritableFile, compressor Compressor, location int64) (entriesWritten int, finalLocation uint64, err error) {
 	var (
 		maxSize = int(metadataBlockSize)
 	)
@@ -878,7 +882,7 @@ func writeExportTable(files []*finalizeFileInfo, f util.File, compressor Compres
 }
 
 // writeIDTable write the uidgid table at the given location.
-func writeIDTable(idtable map[uint32]uint16, f util.File, compressor Compressor, location int64) (entriesWritten int, finalLocation uint64, err error) {
+func writeIDTable(idtable map[uint32]uint16, f backend.WritableFile, compressor Compressor, location int64) (entriesWritten int, finalLocation uint64, err error) {
 	var (
 		maxSize = int(metadataBlockSize)
 	)
@@ -938,7 +942,7 @@ func writeIDTable(idtable map[uint32]uint16, f util.File, compressor Compressor,
 }
 
 // writeXattrs write the xattrs and its lookup table at the given location.
-func writeXattrs(xattrs []map[string]string, f util.File, compressor Compressor, location int64) (xattrsWritten int, finalLocation uint64, err error) {
+func writeXattrs(xattrs []map[string]string, f backend.WritableFile, compressor Compressor, location int64) (xattrsWritten int, finalLocation uint64, err error) {
 	var (
 		maxSize     = int(metadataBlockSize)
 		offset      int
diff --git a/filesystem/squashfs/squashfs.go b/filesystem/squashfs/squashfs.go
index 8b769de..71aac05 100644
--- a/filesystem/squashfs/squashfs.go
+++ b/filesystem/squashfs/squashfs.go
@@ -8,8 +8,8 @@ import (
 	"os"
 	"path"
 
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/filesystem"
-	"github.com/diskfs/go-diskfs/util"
 )
 
 const (
@@ -26,7 +26,7 @@ type FileSystem struct {
 	superblock *superblock
 	size       int64
 	start      int64
-	file       util.File
+	backend    backend.Storage
 	blocksize  int64
 	compressor Compressor
 	fragments  []*fragmentEntry
@@ -38,7 +38,7 @@ type FileSystem struct {
 
 // Equal compare if two filesystems are equal
 func (fs *FileSystem) Equal(a *FileSystem) bool {
-	localMatch := fs.file == a.file && fs.size == a.size
+	localMatch := fs.backend == a.backend && fs.size == a.size
 	superblockMatch := fs.superblock.equal(a.superblock)
 	return localMatch && superblockMatch
 }
@@ -59,8 +59,8 @@ func (fs *FileSystem) Workspace() string {
 
 // Create creates a squashfs filesystem in a given directory
 //
-// requires the util.File where to create the filesystem, size is the size of the filesystem in bytes,
-// start is how far in bytes from the beginning of the util.File to create the filesystem,
+// requires the backend.Storage where to create the filesystem, size is the size of the filesystem in bytes,
+// start is how far in bytes from the beginning of the backend.Storage to create the filesystem,
 // and blocksize is is the logical blocksize to use for creating the filesystem
 //
 // note that you are *not* required to create the filesystem on the entire disk. You could have a disk of size
@@ -72,7 +72,7 @@ func (fs *FileSystem) Workspace() string {
 // where a partition starts and ends.
 //
 // If the provided blocksize is 0, it will use the default of 128 KB.
-func Create(f util.File, size, start, blocksize int64) (*FileSystem, error) {
+func Create(b backend.Storage, size, start, blocksize int64) (*FileSystem, error) {
 	if blocksize == 0 {
 		blocksize = defaultBlockSize
 	}
@@ -94,15 +94,15 @@ func Create(f util.File, size, start, blocksize int64) (*FileSystem, error) {
 		workspace: tmpdir,
 		start:     start,
 		size:      size,
-		file:      f,
+		backend:   b,
 		blocksize: blocksize,
 	}, nil
 }
 
 // Read reads a filesystem from a given disk.
 //
-// requires the util.File where to read the filesystem, size is the size of the filesystem in bytes,
-// start is how far in bytes from the beginning of the util.File the filesystem is expected to begin,
+// requires the backend.Storage where to read the filesystem, size is the size of the filesystem in bytes,
+// start is how far in bytes from the beginning of the backend.Storage the filesystem is expected to begin,
 // and blocksize is is the logical blocksize to use for creating the filesystem
 //
 // note that you are *not* required to read a filesystem on the entire disk. You could have a disk of size
@@ -145,7 +145,7 @@ func Create(f util.File, size, start, blocksize int64) (*FileSystem, error) {
 // uses this library like this:
 //
 //	rclone -P --transfers 16 --checkers 16 copy :archive:/path/to/tensorflow.sqfs /tmp/tensorflow
-func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
+func Read(b backend.Storage, size, start, blocksize int64) (*FileSystem, error) {
 	var (
 		read int
 		err  error
@@ -162,8 +162,8 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 	// load the information from the disk
 
 	// read the superblock
-	b := make([]byte, superblockSize)
-	read, err = file.ReadAt(b, start)
+	superblockBytes := make([]byte, superblockSize)
+	read, err = b.ReadAt(superblockBytes, start)
 	if err != nil {
 		return nil, fmt.Errorf("unable to read bytes for superblock: %v", err)
 	}
@@ -172,7 +172,7 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 	}
 
 	// parse superblock
-	s, err := parseSuperblock(b)
+	s, err := parseSuperblock(superblockBytes)
 	if err != nil {
 		return nil, fmt.Errorf("error parsing superblock: %v", err)
 	}
@@ -184,7 +184,7 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 	}
 
 	// load fragments
-	fragments, err := readFragmentTable(s, file, compress)
+	fragments, err := readFragmentTable(s, b, compress)
 	if err != nil {
 		return nil, fmt.Errorf("error reading fragments: %v", err)
 	}
@@ -195,14 +195,14 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 	)
 	if !s.noXattrs && s.xattrTableStart != 0xffff_ffff_ffff_ffff {
 		// xattr is right to the end of the disk
-		xattrs, err = readXattrsTable(s, file, compress)
+		xattrs, err = readXattrsTable(s, b, compress)
 		if err != nil {
 			return nil, fmt.Errorf("error reading xattr table: %v", err)
 		}
 	}
 
 	// read uidsgids
-	uidsgids, err := readUidsGids(s, file, compress)
+	uidsgids, err := readUidsGids(s, b, compress)
 	if err != nil {
 		return nil, fmt.Errorf("error reading uids/gids: %v", err)
 	}
@@ -211,7 +211,7 @@ func Read(file util.File, size, start, blocksize int64) (*FileSystem, error) {
 		workspace:  "", // no workspace when we do nothing with it
 		start:      start,
 		size:       size,
-		file:       file,
+		backend:    b,
 		superblock: s,
 		blocksize:  int64(s.blocksize), // use the blocksize in the superblock
 		xattrs:     xattrs,
@@ -562,7 +562,7 @@ func (fs *FileSystem) getInode(blockOffset uint32, byteOffset uint16, iType inod
 	// get the block
 	// start by getting the minimum for the proposed type. It very well might be wrong.
 	size := inodeTypeToSize(iType)
-	uncompressed, err := fs.readMetadata(fs.file, fs.compressor, int64(fs.superblock.inodeTableStart), blockOffset, byteOffset, size)
+	uncompressed, err := fs.readMetadata(fs.backend, fs.compressor, int64(fs.superblock.inodeTableStart), blockOffset, byteOffset, size)
 	if err != nil {
 		return nil, fmt.Errorf("error reading block at position %d: %v", blockOffset, err)
 	}
@@ -576,7 +576,7 @@ func (fs *FileSystem) getInode(blockOffset uint32, byteOffset uint16, iType inod
 		size = inodeTypeToSize(iType)
 		// Read more data if necessary (quite rare)
 		if size > len(uncompressed) {
-			uncompressed, err = fs.readMetadata(fs.file, fs.compressor, int64(fs.superblock.inodeTableStart), blockOffset, byteOffset, size)
+			uncompressed, err = fs.readMetadata(fs.backend, fs.compressor, int64(fs.superblock.inodeTableStart), blockOffset, byteOffset, size)
 			if err != nil {
 				return nil, fmt.Errorf("error reading block at position %d: %v", blockOffset, err)
 			}
@@ -590,7 +590,7 @@ func (fs *FileSystem) getInode(blockOffset uint32, byteOffset uint16, iType inod
 	// if it returns extra > 0, then it needs that many more bytes to be read, and to be reparsed
 	if extra > 0 {
 		size += extra
-		uncompressed, err = fs.readMetadata(fs.file, fs.compressor, int64(fs.superblock.inodeTableStart), blockOffset, byteOffset, size)
+		uncompressed, err = fs.readMetadata(fs.backend, fs.compressor, int64(fs.superblock.inodeTableStart), blockOffset, byteOffset, size)
 		if err != nil {
 			return nil, fmt.Errorf("error reading block at position %d: %v", blockOffset, err)
 		}
@@ -610,7 +610,7 @@ func (fs *FileSystem) getInode(blockOffset uint32, byteOffset uint16, iType inod
 // block when uncompressed.
 func (fs *FileSystem) getDirectory(blockOffset uint32, byteOffset uint16, size int) (*directory, error) {
 	// get the block
-	uncompressed, err := fs.readMetadata(fs.file, fs.compressor, int64(fs.superblock.directoryTableStart), blockOffset, byteOffset, size)
+	uncompressed, err := fs.readMetadata(fs.backend, fs.compressor, int64(fs.superblock.directoryTableStart), blockOffset, byteOffset, size)
 	if err != nil {
 		return nil, fmt.Errorf("error reading block at position %d: %v", blockOffset, err)
 	}
@@ -628,7 +628,7 @@ func (fs *FileSystem) readBlock(location int64, compressed bool, size uint32) ([
 		return make([]byte, fs.superblock.blocksize), nil
 	}
 	b := make([]byte, size)
-	read, err := fs.file.ReadAt(b, location)
+	read, err := fs.backend.ReadAt(b, location)
 	if err != nil && err != io.EOF {
 		return nil, fmt.Errorf("error reading block %d: %v", location, err)
 	}
@@ -657,7 +657,7 @@ func (fs *FileSystem) readFragment(index, offset uint32, fragmentSize int64) ([]
 	data, _, err := fs.cache.get(pos, func() (data []byte, size uint16, err error) {
 		// figure out the size of the compressed block and if it is compressed
 		b := make([]byte, fragmentInfo.size)
-		read, err := fs.file.ReadAt(b, pos)
+		read, err := fs.backend.ReadAt(b, pos)
 		if err != nil && err != io.EOF {
 			return nil, 0, fmt.Errorf("unable to read fragment block %d: %v", index, err)
 		}
@@ -698,7 +698,7 @@ func validateBlocksize(blocksize int64) error {
 	return nil
 }
 
-func readFragmentTable(s *superblock, file util.File, c Compressor) ([]*fragmentEntry, error) {
+func readFragmentTable(s *superblock, file backend.File, c Compressor) ([]*fragmentEntry, error) {
 	// get the first level index, which is just the pointers to the fragment table metadata blocks
 	blockCount := s.fragmentCount / 512
 	if s.fragmentCount%512 > 0 {
@@ -755,7 +755,7 @@ To read the xattr table:
 6- Read the id metablocks based on the indexes and uncompress if needed
 7- Read all of the xattr metadata. It starts at the location indicated by the header, and ends at the id table
 */
-func readXattrsTable(s *superblock, file util.File, c Compressor) (*xAttrTable, error) {
+func readXattrsTable(s *superblock, file backend.File, c Compressor) (*xAttrTable, error) {
 	// first read the header
 	b := make([]byte, xAttrHeaderSize)
 	read, err := file.ReadAt(b, int64(s.xattrTableStart))
@@ -858,7 +858,7 @@ To read the uids/gids table:
 4- Read the indexes. They are uncompressed, 8 bytes each (uint64); one index per id metablock
 5- Read the id metablocks based on the indexes and uncompress if needed
 */
-func readUidsGids(s *superblock, file util.File, c Compressor) ([]uint32, error) {
+func readUidsGids(s *superblock, file backend.File, c Compressor) ([]uint32, error) {
 	// find out how many xattr IDs we have and where the metadata starts. The table always starts
 	//   with this information
 	idStart := s.idTableStart
diff --git a/go.mod b/go.mod
index 786440a..1d17436 100644
--- a/go.mod
+++ b/go.mod
@@ -6,15 +6,14 @@ require (
 	github.com/djherbis/times v1.6.0
 	github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab
 	github.com/go-test/deep v1.0.8
+	github.com/google/go-cmp v0.6.0
 	github.com/google/uuid v1.3.0
+	github.com/klauspost/compress v1.17.4
 	github.com/pierrec/lz4/v4 v4.1.17
 	github.com/pkg/xattr v0.4.9
 	github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
 	github.com/ulikunitz/xz v0.5.11
-	golang.org/x/sys v0.5.0
+	golang.org/x/sys v0.19.0
 )
 
-require (
-	github.com/google/go-cmp v0.6.0
-	github.com/klauspost/compress v1.17.4
-)
+require github.com/stretchr/testify v1.7.1 // indirect
diff --git a/go.sum b/go.sum
index ebd2799..bc7a3fc 100644
--- a/go.sum
+++ b/go.sum
@@ -22,15 +22,16 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
 github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
 github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/partition/gpt/partition.go b/partition/gpt/partition.go
index bfa0e66..82ee7ab 100644
--- a/partition/gpt/partition.go
+++ b/partition/gpt/partition.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"unicode/utf16"
 
-	"github.com/diskfs/go-diskfs/util"
+	"github.com/diskfs/go-diskfs/backend"
 	uuid "github.com/google/uuid"
 )
 
@@ -147,7 +147,7 @@ func (p *Partition) GetStart() int64 {
 
 // WriteContents fills the partition with the contents provided
 // reads from beginning of reader to exactly size of partition in bytes
-func (p *Partition) WriteContents(f util.File, contents io.Reader) (uint64, error) {
+func (p *Partition) WriteContents(f backend.WritableFile, contents io.Reader) (uint64, error) {
 	pss, lss := p.sectorSizes()
 	total := uint64(0)
 	// validate start/end/size
@@ -202,7 +202,7 @@ func (p *Partition) WriteContents(f util.File, contents io.Reader) (uint64, erro
 
 // ReadContents reads the contents of the partition into a writer
 // streams the entire partition to the writer
-func (p *Partition) ReadContents(f util.File, out io.Writer) (int64, error) {
+func (p *Partition) ReadContents(f backend.File, out io.Writer) (int64, error) {
 	pss, _ := p.sectorSizes()
 	total := int64(0)
 	// chunks of physical sector size for efficient writing
diff --git a/partition/gpt/table.go b/partition/gpt/table.go
index 5d62311..af01c3f 100644
--- a/partition/gpt/table.go
+++ b/partition/gpt/table.go
@@ -7,8 +7,8 @@ import (
 	"hash/crc32"
 	"strings"
 
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/partition/part"
-	"github.com/diskfs/go-diskfs/util"
 	uuid "github.com/google/uuid"
 )
 
@@ -462,8 +462,8 @@ func (t *Table) Type() string {
 }
 
 // Write writes a GPT to disk
-// Must be passed the util.File to which to write and the size of the disk
-func (t *Table) Write(f util.File, size int64) error {
+// Must be passed the backend.WritableFile to which to write and the size of the disk
+func (t *Table) Write(f backend.WritableFile, size int64) error {
 	// it is possible that we are given a basic new table that we need to initialize
 	if !t.initialized {
 		t.initTable(size)
@@ -536,11 +536,11 @@ func (t *Table) Write(f util.File, size int64) error {
 }
 
 // Read read a partition table from a disk
-// must be passed the util.File from which to read, and the logical and physical block sizes
+// must be passed the backend.File from which to read, and the logical and physical block sizes
 //
 // if successful, returns a gpt.Table struct
 // returns errors if fails at any stage reading the disk or processing the bytes on disk as a GPT
-func Read(f util.File, logicalBlockSize, physicalBlockSize int) (*Table, error) {
+func Read(f backend.File, logicalBlockSize, physicalBlockSize int) (*Table, error) {
 	// read the data off of the disk - first block is the compatibility MBR, ssecond is the GPT table
 	b := make([]byte, logicalBlockSize*2)
 	read, err := f.ReadAt(b, 0)
@@ -595,7 +595,7 @@ func (t *Table) UUID() string {
 }
 
 // Verify will attempt to evaluate the headers
-func (t *Table) Verify(f util.File, diskSize uint64) error {
+func (t *Table) Verify(f backend.File, diskSize uint64) error {
 	if t.LogicalSectorSize == 0 {
 		// Avoid divide by zero panic.
 		return fmt.Errorf("table is not initialized")
diff --git a/partition/mbr/partition.go b/partition/mbr/partition.go
index 18ecaaf..8a5468f 100644
--- a/partition/mbr/partition.go
+++ b/partition/mbr/partition.go
@@ -7,7 +7,7 @@ import (
 	"fmt"
 	"io"
 
-	"github.com/diskfs/go-diskfs/util"
+	"github.com/diskfs/go-diskfs/backend"
 )
 
 // Partition represents the structure of a single partition on the disk
@@ -123,7 +123,7 @@ func partitionFromBytes(b []byte, logicalSectorSize, physicalSectorSize int) (*P
 
 // WriteContents fills the partition with the contents provided
 // reads from beginning of reader to exactly size of partition in bytes
-func (p *Partition) WriteContents(f util.File, contents io.Reader) (uint64, error) {
+func (p *Partition) WriteContents(f backend.WritableFile, contents io.Reader) (uint64, error) {
 	pss, lss := p.sectorSizes()
 	total := uint64(0)
 
@@ -166,7 +166,7 @@ func (p *Partition) WriteContents(f util.File, contents io.Reader) (uint64, erro
 
 // readContents reads the contents of the partition into a writer
 // streams the entire partition to the writer
-func (p *Partition) ReadContents(f util.File, out io.Writer) (int64, error) {
+func (p *Partition) ReadContents(f backend.File, out io.Writer) (int64, error) {
 	pss, lss := p.sectorSizes()
 	total := int64(0)
 	// chunks of physical sector size for efficient writing
diff --git a/partition/mbr/table.go b/partition/mbr/table.go
index c64dab5..4a55f3d 100644
--- a/partition/mbr/table.go
+++ b/partition/mbr/table.go
@@ -5,8 +5,8 @@ import (
 	"encoding/binary"
 	"fmt"
 
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/partition/part"
-	"github.com/diskfs/go-diskfs/util"
 )
 
 // Table represents an MBR partition table to be applied to a disk or read from a disk
@@ -134,7 +134,7 @@ func (t *Table) Type() string {
 // Read read a partition table from a disk, given the logical block size and physical block size
 //
 //nolint:unused,revive // not used in MBR, but it is important to implement the interface
-func Read(f util.File, logicalBlockSize, physicalBlockSize int) (*Table, error) {
+func Read(f backend.File, logicalBlockSize, physicalBlockSize int) (*Table, error) {
 	// read the data off of the disk
 	b := make([]byte, mbrSize)
 	read, err := f.ReadAt(b, 0)
@@ -168,10 +168,10 @@ func (t *Table) toBytes() []byte {
 }
 
 // Write writes a given MBR Table to disk.
-// Must be passed the util.File to write to and the size of the disk
+// Must be passed the backend.WritableFile to write to and the size of the disk
 //
 //nolint:unused,revive // not used in MBR, but it is important to implement the interface
-func (t *Table) Write(f util.File, size int64) error {
+func (t *Table) Write(f backend.WritableFile, size int64) error {
 	b := t.toBytes()
 
 	written, err := f.WriteAt(b, partitionEntriesStart)
@@ -196,7 +196,7 @@ func (t *Table) GetPartitions() []part.Partition {
 // Verify will attempt to evaluate the headers
 //
 //nolint:unused,revive // not used in MBR, but it is important to implement the interface
-func (t *Table) Verify(f util.File, diskSize uint64) error {
+func (t *Table) Verify(f backend.File, diskSize uint64) error {
 	return nil
 }
 
diff --git a/partition/part/partition.go b/partition/part/partition.go
index f87ba9f..1a34f6d 100644
--- a/partition/part/partition.go
+++ b/partition/part/partition.go
@@ -3,14 +3,14 @@ package part
 import (
 	"io"
 
-	"github.com/diskfs/go-diskfs/util"
+	"github.com/diskfs/go-diskfs/backend"
 )
 
 // Partition reference to an individual partition on disk
 type Partition interface {
 	GetSize() int64
 	GetStart() int64
-	ReadContents(util.File, io.Writer) (int64, error)
-	WriteContents(util.File, io.Reader) (uint64, error)
+	ReadContents(backend.File, io.Writer) (int64, error)
+	WriteContents(backend.WritableFile, io.Reader) (uint64, error)
 	UUID() string
 }
diff --git a/partition/partition.go b/partition/partition.go
index d47ffba..627f3a8 100644
--- a/partition/partition.go
+++ b/partition/partition.go
@@ -5,13 +5,13 @@ package partition
 import (
 	"fmt"
 
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/partition/gpt"
 	"github.com/diskfs/go-diskfs/partition/mbr"
-	"github.com/diskfs/go-diskfs/util"
 )
 
 // Read read a partition table from a disk
-func Read(f util.File, logicalBlocksize, physicalBlocksize int) (Table, error) {
+func Read(f backend.File, logicalBlocksize, physicalBlocksize int) (Table, error) {
 	// just try each type
 	gptTable, err := gpt.Read(f, logicalBlocksize, physicalBlocksize)
 	if err == nil {
diff --git a/partition/table.go b/partition/table.go
index 62f5650..91d8959 100644
--- a/partition/table.go
+++ b/partition/table.go
@@ -1,16 +1,16 @@
 package partition
 
 import (
+	"github.com/diskfs/go-diskfs/backend"
 	"github.com/diskfs/go-diskfs/partition/part"
-	"github.com/diskfs/go-diskfs/util"
 )
 
 // Table reference to a partitioning table on disk
 type Table interface {
 	Type() string
-	Write(util.File, int64) error
+	Write(backend.WritableFile, int64) error
 	GetPartitions() []part.Partition
 	Repair(diskSize uint64) error
-	Verify(f util.File, diskSize uint64) error
+	Verify(f backend.File, diskSize uint64) error
 	UUID() string
 }
diff --git a/util/file.go b/util/file.go
deleted file mode 100644
index 78d0a15..0000000
--- a/util/file.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// Package util common utilities or other elements shared across github.com/diskfs/go-diskfs packages
-package util
-
-import "io"
-
-// File interface that can be read from and written to.
-// Normally implemented as actual os.File, but useful as a separate interface so can easily
-// use alternate implementations.
-type File interface {
-	io.ReaderAt
-	io.WriterAt
-	io.Seeker
-}


Test patch:
diff --git a/disk/disk_test.go b/disk/disk_test.go
index 7bb2ce4..6fd4b7d 100644
--- a/disk/disk_test.go
+++ b/disk/disk_test.go
@@ -15,11 +15,13 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/disk"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/partition"
 	"github.com/diskfs/go-diskfs/partition/gpt"
 	"github.com/diskfs/go-diskfs/partition/mbr"
+	"github.com/diskfs/go-diskfs/testhelper"
 )
 
 var (
@@ -70,10 +72,9 @@ func TestGetPartitionTable(t *testing.T) {
 	defer f.Close()
 
 	d := &disk.Disk{
-		File:              f,
+		Backend:           file.New(f, true),
 		LogicalBlocksize:  512,
 		PhysicalBlocksize: 512,
-		Writable:          false,
 	}
 	table, err := d.GetPartitionTable()
 
@@ -109,11 +110,9 @@ func TestPartition(t *testing.T) {
 		}
 
 		d := &disk.Disk{
-			File:              f,
+			Backend:           file.New(f, false),
 			LogicalBlocksize:  512,
 			PhysicalBlocksize: 512,
-			Info:              fileInfo,
-			Writable:          true,
 			Size:              fileInfo.Size(),
 		}
 		// this is partition start and end in sectors, not bytes
@@ -146,17 +145,10 @@ func TestPartition(t *testing.T) {
 			fmt.Println(f.Name())
 		}
 
-		fileInfo, err := f.Stat()
-		if err != nil {
-			t.Fatalf("error reading info on temporary disk: %v", err)
-		}
-
 		d := &disk.Disk{
-			File:              f,
+			Backend:           file.New(f, false),
 			LogicalBlocksize:  512,
 			PhysicalBlocksize: 512,
-			Info:              fileInfo,
-			Writable:          true,
 		}
 		// this is partition start and end in sectors, not bytes
 		sectorSize := 512
@@ -176,7 +168,7 @@ func TestPartition(t *testing.T) {
 	})
 	t.Run("readonly", func(t *testing.T) {
 		d := &disk.Disk{
-			Writable: false,
+			Backend: file.New(&testhelper.FileImpl{}, true),
 		}
 		expectedErr := fmt.Errorf("disk file or device not open for write")
 		err := d.Partition(&mbr.Table{})
@@ -226,18 +218,11 @@ func TestWritePartitionContents(t *testing.T) {
 					fmt.Println(f.Name())
 				}
 
-				fileInfo, err := f.Stat()
-				if err != nil {
-					t.Fatalf("error reading info on temporary disk: %v", err)
-				}
-
 				d := &disk.Disk{
-					File:              f,
+					Backend:           file.New(f, false),
 					LogicalBlocksize:  512,
 					PhysicalBlocksize: 512,
-					Info:              fileInfo,
 					Table:             tt.table,
-					Writable:          true,
 				}
 				b := make([]byte, partitionSize)
 				_, _ = rand.Read(b)
@@ -256,7 +241,7 @@ func TestWritePartitionContents(t *testing.T) {
 	})
 	t.Run("readonly", func(t *testing.T) {
 		d := &disk.Disk{
-			Writable: false,
+			Backend: file.New(&testhelper.FileImpl{}, true),
 		}
 		expectedErr := fmt.Errorf("disk file or device not open for write")
 		_, err := d.WritePartitionContents(0, nil)
@@ -266,7 +251,6 @@ func TestWritePartitionContents(t *testing.T) {
 	})
 }
 
-//nolint:gocyclo // we do not care much about cyclomatic complexity in the test function. Maybe someday we can improve it.
 func TestReadPartitionContents(t *testing.T) {
 	t.Run("gpt", func(t *testing.T) {
 		partitionStart := uint64(2048)
@@ -308,22 +292,15 @@ func TestReadPartitionContents(t *testing.T) {
 					fmt.Println(f.Name())
 				}
 
-				fileInfo, err := f.Stat()
-				if err != nil {
-					t.Fatalf("error reading info on temporary disk: %v", err)
-				}
-
 				// get the actual content
 				b2 := make([]byte, partitionSize*512)
 				_, _ = f.ReadAt(b2, int64(partitionStart*512))
 
 				d := &disk.Disk{
-					File:              f,
+					Backend:           file.New(f, true),
 					LogicalBlocksize:  512,
 					PhysicalBlocksize: 512,
-					Info:              fileInfo,
 					Table:             tt.table,
-					Writable:          false,
 				}
 				var writer bytes.Buffer
 				read, err := d.ReadPartitionContents(tt.partition, &writer)
@@ -381,22 +358,15 @@ func TestReadPartitionContents(t *testing.T) {
 					fmt.Println(f.Name())
 				}
 
-				fileInfo, err := f.Stat()
-				if err != nil {
-					t.Fatalf("error reading info on temporary disk: %v", err)
-				}
-
 				// get the actual content
 				b2 := make([]byte, partitionSize*512)
 				_, _ = f.ReadAt(b2, int64(partitionStart*512))
 
 				d := &disk.Disk{
-					File:              f,
+					Backend:           file.New(f, true),
 					LogicalBlocksize:  512,
 					PhysicalBlocksize: 512,
-					Info:              fileInfo,
 					Table:             tt.table,
-					Writable:          false,
 				}
 				var writer bytes.Buffer
 				read, err := d.ReadPartitionContents(tt.partition, &writer)
@@ -433,17 +403,10 @@ func TestCreateFilesystem(t *testing.T) {
 			fmt.Println(f.Name())
 		}
 
-		fileInfo, err := f.Stat()
-		if err != nil {
-			t.Fatalf("error reading info on temporary disk: %v", err)
-		}
-
 		d := &disk.Disk{
-			File:              f,
+			Backend:           file.New(f, false),
 			LogicalBlocksize:  512,
 			PhysicalBlocksize: 512,
-			Info:              fileInfo,
-			Writable:          true,
 		}
 		expected := fmt.Errorf("cannot create filesystem on a partition without a partition table")
 		fs, err := d.CreateFilesystem(disk.FilesystemSpec{Partition: 1, FSType: filesystem.TypeFat32})
@@ -473,12 +436,10 @@ func TestCreateFilesystem(t *testing.T) {
 		}
 
 		d := &disk.Disk{
-			File:              f,
+			Backend:           file.New(f, false),
 			LogicalBlocksize:  512,
 			PhysicalBlocksize: 512,
-			Info:              fileInfo,
 			Size:              fileInfo.Size(),
-			Writable:          true,
 		}
 		fs, err := d.CreateFilesystem(disk.FilesystemSpec{Partition: 0, FSType: filesystem.TypeFat32})
 		if err != nil {
@@ -515,13 +476,11 @@ func TestCreateFilesystem(t *testing.T) {
 			LogicalSectorSize: 512,
 		}
 		d := &disk.Disk{
-			File:              f,
+			Backend:           file.New(f, false),
 			LogicalBlocksize:  512,
 			PhysicalBlocksize: 512,
-			Info:              fileInfo,
 			Size:              fileInfo.Size(),
 			Table:             table,
-			Writable:          true,
 		}
 		fs, err := d.CreateFilesystem(disk.FilesystemSpec{Partition: 1, FSType: filesystem.TypeFat32})
 		if err != nil {
@@ -533,7 +492,7 @@ func TestCreateFilesystem(t *testing.T) {
 	})
 	t.Run("readonly", func(t *testing.T) {
 		d := &disk.Disk{
-			Writable: false,
+			Backend: file.New(&testhelper.FileImpl{}, true),
 		}
 		expectedErr := fmt.Errorf("disk file or device not open for write")
 		_, err := d.CreateFilesystem(disk.FilesystemSpec{})
@@ -557,17 +516,10 @@ func TestGetFilesystem(t *testing.T) {
 			fmt.Println(f.Name())
 		}
 
-		fileInfo, err := f.Stat()
-		if err != nil {
-			t.Fatalf("error reading info on temporary disk: %v", err)
-		}
-
 		d := &disk.Disk{
-			File:              f,
+			Backend:           file.New(f, true),
 			LogicalBlocksize:  512,
 			PhysicalBlocksize: 512,
-			Info:              fileInfo,
-			Writable:          false,
 		}
 		expected := fmt.Errorf("cannot read filesystem on a partition without a partition table")
 		fs, err := d.GetFilesystem(1)
@@ -609,12 +561,10 @@ func TestGetFilesystem(t *testing.T) {
 		}
 
 		d := &disk.Disk{
-			File:              f,
+			Backend:           file.New(f, true),
 			LogicalBlocksize:  512,
 			PhysicalBlocksize: 512,
-			Info:              fileInfo,
 			Size:              fileInfo.Size(),
-			Writable:          false,
 		}
 		fs, err := d.GetFilesystem(0)
 		if err != nil {
@@ -651,13 +601,11 @@ func TestGetFilesystem(t *testing.T) {
 			LogicalSectorSize: 512,
 		}
 		d := &disk.Disk{
-			File:              f,
+			Backend:           file.New(f, true),
 			LogicalBlocksize:  512,
 			PhysicalBlocksize: 512,
-			Info:              fileInfo,
 			Size:              fileInfo.Size(),
 			Table:             table,
-			Writable:          false,
 		}
 		fs, err := d.GetFilesystem(1)
 		if err != nil {
diff --git a/diskfs_test.go b/diskfs_test.go
index 314be09..245390e 100644
--- a/diskfs_test.go
+++ b/diskfs_test.go
@@ -60,7 +60,7 @@ func checkDiskfsErrs(t *testing.T, msg string, err, tterr error, d, ttd *disk.Di
 		t.Errorf("%s: mismatched errors, actual %v expected %v", msg, err, tterr)
 	case (d == nil && ttd != nil) || (d != nil && ttd == nil):
 		t.Errorf("%s: mismatched disk, actual %v expected %v", msg, d, ttd)
-	case d != nil && (d.LogicalBlocksize != ttd.LogicalBlocksize || d.PhysicalBlocksize != ttd.PhysicalBlocksize || d.Size != ttd.Size || d.Type != ttd.Type):
+	case d != nil && (d.LogicalBlocksize != ttd.LogicalBlocksize || d.PhysicalBlocksize != ttd.PhysicalBlocksize || d.Size != ttd.Size):
 		t.Errorf("%s: mismatched disk, actual then expected", msg)
 		t.Logf("%v", d)
 		t.Logf("%v", ttd)
@@ -103,22 +103,28 @@ func TestGPTOpen(t *testing.T) {
 	}{
 		{"", nil, fmt.Errorf("must pass device name")},
 		{"/tmp/foo/bar/232323/23/2322/disk.img", nil, fmt.Errorf("")},
-		{path, &disk.Disk{Type: disk.File, LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: size}, nil},
-		{filePadded, &disk.Disk{Type: disk.File, LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: filePaddedSize}, nil},
+		{path, &disk.Disk{LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: size}, nil},
+		{filePadded, &disk.Disk{LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: filePaddedSize}, nil},
 	}
 
 	for _, tt := range tests {
 		d, err := diskfs.Open(tt.path)
+
 		msg := fmt.Sprintf("Open(%s)", tt.path)
 		checkDiskfsErrs(t, msg, err, tt.err, d, tt.disk)
+
 		if d != nil {
 			table, err := d.GetPartitionTable()
 			if err != nil {
 				t.Errorf("%s: mismatched errors, actual %v expected %v", msg, err, tt.err)
 			}
 
+			backingFile, err := d.Backend.Writable()
+			if err != nil {
+				t.Fatal(err)
+			}
 			// Verify will compare the GPT table to the disk and attempt to read the secondary header if possible
-			err = table.Verify(d.File, uint64(tt.disk.Size))
+			err = table.Verify(backingFile, uint64(tt.disk.Size))
 			if err != nil {
 				// We log this as it's epected to be an error
 				t.Logf("%s: mismatched errors, actual %v expected %v", msg, err, tt.err)
@@ -131,13 +137,13 @@ func TestGPTOpen(t *testing.T) {
 			}
 
 			// Update both tables on disk
-			err = table.Write(d.File, tt.disk.Size)
+			err = table.Write(backingFile, tt.disk.Size)
 			if err != nil {
 				t.Errorf("%s: mismatched errors, actual %v expected %v", msg, err, tt.err)
 			}
 
 			// Check that things are as expected.
-			err = table.Verify(d.File, uint64(tt.disk.Size))
+			err = table.Verify(backingFile, uint64(tt.disk.Size))
 			if err != nil {
 				t.Errorf("%s: mismatched errors, actual %v expected %v", msg, err, tt.err)
 			}
@@ -172,7 +178,7 @@ func TestMBROpen(t *testing.T) {
 	}{
 		{"", nil, fmt.Errorf("must pass device name")},
 		{"/tmp/foo/bar/232323/23/2322/disk.img", nil, fmt.Errorf("")},
-		{path, &disk.Disk{Type: disk.File, LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: size}, nil},
+		{path, &disk.Disk{LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: size}, nil},
 	}
 
 	// open default
@@ -195,18 +201,17 @@ func TestCreate(t *testing.T) {
 		name       string
 		path       string
 		size       int64
-		format     diskfs.Format
 		sectorSize diskfs.SectorSize
 		disk       *disk.Disk
 		err        error
 	}{
-		{"no file", "", 10 * oneMB, diskfs.Raw, diskfs.SectorSizeDefault, nil, fmt.Errorf("must pass device name")},
-		{"zero size", "disk", 0, diskfs.Raw, diskfs.SectorSizeDefault, nil, fmt.Errorf("must pass valid device size to create")},
-		{"negative size", "disk", -1, diskfs.Raw, diskfs.SectorSizeDefault, nil, fmt.Errorf("must pass valid device size to create")},
-		{"directory does not exist", "foo/bar/232323/23/2322/disk", 10 * oneMB, diskfs.Raw, diskfs.SectorSizeDefault, nil, fmt.Errorf("could not create device")},
-		{"10MB with default sector size", "disk", 10 * oneMB, diskfs.Raw, diskfs.SectorSizeDefault, &disk.Disk{LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: 10 * oneMB, Type: disk.File}, nil},
-		{"10MB with 512 sector size", "disk", 10 * oneMB, diskfs.Raw, diskfs.SectorSize512, &disk.Disk{LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: 10 * oneMB, Type: disk.File}, nil},
-		{"10MB with 2048 sector size", "disk", 10 * oneMB, diskfs.Raw, diskfs.SectorSize4k, &disk.Disk{LogicalBlocksize: 4096, PhysicalBlocksize: 4096, Size: 10 * oneMB, Type: disk.File}, nil},
+		{"no file", "", 10 * oneMB, diskfs.SectorSizeDefault, nil, fmt.Errorf("must pass device name")},
+		{"zero size", "disk", 0, diskfs.SectorSizeDefault, nil, fmt.Errorf("must pass valid device size to create")},
+		{"negative size", "disk", -1, diskfs.SectorSizeDefault, nil, fmt.Errorf("must pass valid device size to create")},
+		{"directory does not exist", "foo/bar/232323/23/2322/disk", 10 * oneMB, diskfs.SectorSizeDefault, nil, fmt.Errorf("could not create device")},
+		{"10MB with default sector size", "disk", 10 * oneMB, diskfs.SectorSizeDefault, &disk.Disk{LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: 10 * oneMB}, nil},
+		{"10MB with 512 sector size", "disk", 10 * oneMB, diskfs.SectorSize512, &disk.Disk{LogicalBlocksize: 512, PhysicalBlocksize: 512, Size: 10 * oneMB}, nil},
+		{"10MB with 2048 sector size", "disk", 10 * oneMB, diskfs.SectorSize4k, &disk.Disk{LogicalBlocksize: 4096, PhysicalBlocksize: 4096, Size: 10 * oneMB}, nil},
 	}
 
 	for i, t2 := range tests {
@@ -216,9 +221,9 @@ func TestCreate(t *testing.T) {
 			if tt.path != "" {
 				filename = testTmpFilename(t, "diskfs_test"+tt.path, ".img")
 			}
-			d, err := diskfs.Create(filename, tt.size, tt.format, tt.sectorSize)
+			d, err := diskfs.Create(filename, tt.size, tt.sectorSize)
 			defer os.RemoveAll(filename)
-			msg := fmt.Sprintf("%d: Create(%s, %d, %v, %d)", i, filename, tt.size, tt.format, tt.sectorSize)
+			msg := fmt.Sprintf("%d: Create(%s, %d, %v, %d)", i, filename, tt.size, 0, tt.sectorSize)
 			checkDiskfsErrs(t, msg, err, tt.err, d, tt.disk)
 		})
 	}
diff --git a/example_test.go b/example_test.go
index 6b99bac..9e00326 100644
--- a/example_test.go
+++ b/example_test.go
@@ -6,6 +6,7 @@ import (
 	"os"
 
 	diskfs "github.com/diskfs/go-diskfs"
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/disk"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/partition/gpt"
@@ -27,7 +28,7 @@ func ExampleCreate_fat32() {
 
 	diskImg := "/tmp/disk.img"
 	defer os.Remove(diskImg)
-	theDisk, _ := diskfs.Create(diskImg, size, diskfs.Raw, diskfs.SectorSizeDefault)
+	theDisk, _ := diskfs.Create(diskImg, size, diskfs.SectorSizeDefault)
 
 	fs, err := theDisk.CreateFilesystem(disk.FilesystemSpec{
 		Partition: 0,
@@ -44,7 +45,7 @@ func ExampleCreate_mbr() {
 
 	diskImg := "/tmp/disk.img"
 	defer os.Remove(diskImg)
-	theDisk, _ := diskfs.Create(diskImg, size, diskfs.Raw, diskfs.SectorSizeDefault)
+	theDisk, _ := diskfs.Create(diskImg, size, diskfs.SectorSizeDefault)
 
 	table := &mbr.Table{
 		LogicalSectorSize:  512,
@@ -75,7 +76,7 @@ func ExampleCreate_gpt() {
 
 	diskImg := "/tmp/disk.img"
 	defer os.Remove(diskImg)
-	theDisk, _ := diskfs.Create(diskImg, size, diskfs.Raw, diskfs.SectorSizeDefault)
+	theDisk, _ := diskfs.Create(diskImg, size, diskfs.SectorSizeDefault)
 
 	table := &gpt.Table{
 		LogicalSectorSize:  512,
@@ -106,7 +107,7 @@ func ExampleCreate_fat32WithDirsAndFiles() {
 
 	diskImg := "/tmp/disk.img"
 	defer os.Remove(diskImg)
-	theDisk, _ := diskfs.Create(diskImg, size, diskfs.Raw, diskfs.SectorSizeDefault)
+	theDisk, _ := diskfs.Create(diskImg, size, diskfs.SectorSizeDefault)
 
 	table := &mbr.Table{
 		LogicalSectorSize:  512,
@@ -143,3 +144,46 @@ func ExampleCreate_fat32WithDirsAndFiles() {
 	check(err)
 	unused(written)
 }
+
+// Create a disk of size 20MB at provided pathname using backend abstraction.
+// This is a proposed way of usage after factoring out underlying file operations to the backend.
+// diskfs.Create() is slowly deprecating now.
+func ExampleOpenBackend_create() {
+	theBackend, err := file.CreateFromPath("/tmp/my.img", 20*1024*1024)
+	check(err)
+
+	theDisk, err := diskfs.OpenBackend(theBackend)
+	check(err)
+	unused(theDisk)
+}
+
+// Open existing image using backend abstraction
+// This is a proposed way of usage after factoring out underlying file operations to the backend.
+// diskfs.Open() is slowly deprecating now.
+func ExampleOpenBackend_open() {
+	readOnly := true
+	theBackend, err := file.OpenFromPath("/tmp/my.img", readOnly)
+	check(err)
+	defer os.Remove("/tmp/my.img")
+
+	theDisk, err := diskfs.OpenBackend(theBackend)
+	check(err)
+	unused(theDisk)
+}
+
+// Instantiate the backend manually for later use in diskfs.OpenBackend
+func ExampleOpenBackend_backend() {
+	// some exemplary fs.FS
+	dirfs := os.DirFS("/tmp/images")
+	// the fs.File we'll use
+	f, err := dirfs.Open("theImage.raw")
+	check(err)
+
+	theBackend := file.New(f, false)
+
+	// As opposed to File example, theBackend will be used as is (i.e. not wrapped).
+	// This usage scenario is for upcoming support for other backends (qcow2 and such).
+	theDisk, err := diskfs.OpenBackend(theBackend)
+	check(err)
+	unused(theDisk)
+}
diff --git a/filesystem/ext4/ext4_test.go b/filesystem/ext4/ext4_test.go
index b9d72c7..d8fdde6 100644
--- a/filesystem/ext4/ext4_test.go
+++ b/filesystem/ext4/ext4_test.go
@@ -13,6 +13,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/go-test/deep"
 )
 
@@ -48,7 +49,9 @@ func TestReadDirectory(t *testing.T) {
 		t.Fatalf("Error opening test image: %v", err)
 	}
 	defer f.Close()
-	fs, err := Read(f, 100*MB, 0, 512)
+
+	b := file.New(f, true)
+	fs, err := Read(b, 100*MB, 0, 512)
 	if err != nil {
 		t.Fatalf("Error reading filesystem: %v", err)
 	}
@@ -105,7 +108,9 @@ func TestReadFile(t *testing.T) {
 		t.Fatalf("Error opening test image: %v", err)
 	}
 	defer f.Close()
-	fs, err := Read(f, 100*MB, 0, 512)
+
+	b := file.New(f, true)
+	fs, err := Read(b, 100*MB, 0, 512)
 	if err != nil {
 		t.Fatalf("Error reading filesystem: %v", err)
 	}
@@ -211,11 +216,13 @@ func TestWriteFile(t *testing.T) {
 				t.Fatalf("Error opening test image: %v", err)
 			}
 			defer f.Close()
-			fs, err := Read(f, 100*MB, 0, 512)
+
+			b := file.New(f, false)
+			fs, err := Read(b, 100*MB, 0, 512)
 			if err != nil {
 				t.Fatalf("Error reading filesystem: %v", err)
 			}
-			file, err := fs.OpenFile(tt.path, tt.flag)
+			ext4File, err := fs.OpenFile(tt.path, tt.flag)
 			switch {
 			case err != nil && tt.err == nil:
 				t.Fatalf("unexpected error opening file: %v", err)
@@ -224,10 +231,10 @@ func TestWriteFile(t *testing.T) {
 			case err != nil && tt.err != nil && !strings.HasPrefix(err.Error(), tt.err.Error()):
 				t.Fatalf("mismatched error opening file, expected '%v' got '%v'", tt.err, err)
 			case err == nil:
-				if _, err := file.Seek(tt.offset, io.SeekStart); err != nil {
+				if _, err := ext4File.Seek(tt.offset, io.SeekStart); err != nil {
 					t.Fatalf("Error seeking file for write: %v", err)
 				}
-				n, err := file.Write(tt.expected)
+				n, err := ext4File.Write(tt.expected)
 				if err != nil && err != io.EOF {
 					t.Fatalf("Error writing file: %v", err)
 				}
@@ -235,11 +242,11 @@ func TestWriteFile(t *testing.T) {
 					t.Fatalf("short write, expected %d bytes got %d", len(tt.expected), n)
 				}
 				// now read from the file and see that it matches what we wrote
-				if _, err := file.Seek(tt.offset, io.SeekStart); err != nil {
+				if _, err := ext4File.Seek(tt.offset, io.SeekStart); err != nil {
 					t.Fatalf("Error seeking file for read: %v", err)
 				}
 				b := make([]byte, len(tt.expected))
-				n, err = file.Read(b)
+				n, err = ext4File.Read(b)
 				if err != nil && err != io.EOF {
 					t.Fatalf("Error reading file: %v", err)
 				}
@@ -276,7 +283,9 @@ func TestRm(t *testing.T) {
 				t.Fatalf("Error opening test image: %v", err)
 			}
 			defer f.Close()
-			fs, err := Read(f, 100*MB, 0, 512)
+
+			b := file.New(f, false)
+			fs, err := Read(b, 100*MB, 0, 512)
 			if err != nil {
 				t.Fatalf("Error reading filesystem: %v", err)
 			}
@@ -319,7 +328,9 @@ func TestTruncateFile(t *testing.T) {
 				t.Fatalf("Error opening test image: %v", err)
 			}
 			defer f.Close()
-			fs, err := Read(f, 100*MB, 0, 512)
+
+			b := file.New(f, false)
+			fs, err := Read(b, 100*MB, 0, 512)
 			if err != nil {
 				t.Fatalf("Error reading filesystem: %v", err)
 			}
@@ -379,7 +390,9 @@ func TestMkdir(t *testing.T) {
 				t.Fatalf("Error opening test image: %v", err)
 			}
 			defer f.Close()
-			fs, err := Read(f, 100*MB, 0, 512)
+
+			b := file.New(f, false)
+			fs, err := Read(b, 100*MB, 0, 512)
 			if err != nil {
 				t.Fatalf("Error reading filesystem: %v", err)
 			}
diff --git a/filesystem/fat32/fat32_internal_test.go b/filesystem/fat32/fat32_internal_test.go
index d2b8242..0f96e28 100644
--- a/filesystem/fat32/fat32_internal_test.go
+++ b/filesystem/fat32/fat32_internal_test.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/testhelper"
 	"github.com/google/go-cmp/cmp"
 )
@@ -70,12 +71,12 @@ func getValidFat32FSSmall() *FileSystem {
 		},
 		bytesPerCluster: 512,
 		dataStart:       178176,
-		file: &testhelper.FileImpl{
+		backend: file.New(&testhelper.FileImpl{
 			//nolint:revive // unused parameter, keeping name makes it easier to use in the future
 			Writer: func(b []byte, offset int64) (int, error) {
 				return len(b), nil
 			},
-		},
+		}, false),
 		fsis: FSInformationSector{},
 		bootSector: msDosBootSector{
 			biosParameterBlock: &dos71EBPB{
@@ -129,14 +130,14 @@ func TestFat32ReadDirectory(t *testing.T) {
 	// will use the fat32.img fixture to test an actual directory
 	// \ (root directory) should be in one cluster
 	// \foo should be in two clusters
-	file, err := os.Open(Fat32File)
+	testFile, err := os.Open(Fat32File)
 	if err != nil {
 		t.Fatalf("could not open file %s to read: %v", Fat32File, err)
 	}
-	defer file.Close()
+	defer testFile.Close()
 	fs := &FileSystem{
 		table:           *getValidFat32Table(),
-		file:            file,
+		backend:         file.New(testFile, false),
 		bytesPerCluster: int(fsInfo.bytesPerCluster),
 		dataStart:       fsInfo.dataStartBytes,
 	}
@@ -337,7 +338,7 @@ func TestFat32ReadDirWithMkdir(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		fs.file = &testhelper.FileImpl{
+		fs.backend = file.New(&testhelper.FileImpl{
 			//nolint:revive // unused parameter, keeping name makes it easier to use in the future
 			Writer: func(b []byte, offset int64) (int, error) {
 				return len(b), nil
@@ -346,7 +347,8 @@ func TestFat32ReadDirWithMkdir(t *testing.T) {
 				copy(b, datab[offset:])
 				return len(b), nil
 			},
-		}
+		}, false)
+
 		dir, entries, err := fs.readDirWithMkdir(tt.path, tt.doMake)
 		switch {
 		case (err == nil && tt.err != nil) || (err != nil && tt.err == nil) || (err != nil && tt.err != nil && !strings.HasPrefix(err.Error(), tt.err.Error())):
diff --git a/filesystem/fat32/fat32_test.go b/filesystem/fat32/fat32_test.go
index 70e822f..bbd4fad 100644
--- a/filesystem/fat32/fat32_test.go
+++ b/filesystem/fat32/fat32_test.go
@@ -18,11 +18,12 @@ import (
 	"testing"
 
 	"github.com/diskfs/go-diskfs"
+	"github.com/diskfs/go-diskfs/backend"
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/disk"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/filesystem/fat32"
 	"github.com/diskfs/go-diskfs/testhelper"
-	"github.com/diskfs/go-diskfs/util"
 )
 
 var (
@@ -116,7 +117,7 @@ func TestFat32Mkdir(t *testing.T) {
 		return
 	}
 	//nolint:thelper // this is not a helper function
-	runTest := func(t *testing.T, post, pre int64, fatFunc func(util.File, int64, int64, int64) (*fat32.FileSystem, error)) {
+	runTest := func(t *testing.T, post, pre int64, fatFunc func(backend.Storage, int64, int64, int64) (*fat32.FileSystem, error)) {
 		// create our directories
 		tests := []string{
 			"/",
@@ -137,7 +138,8 @@ func TestFat32Mkdir(t *testing.T) {
 		if err != nil {
 			t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 		}
-		fs, err := fatFunc(f, fileInfo.Size()-pre-post, pre, 512)
+
+		fs, err := fatFunc(file.New(f, false), fileInfo.Size()-pre-post, pre, 512)
 		if err != nil {
 			t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 		}
@@ -161,7 +163,7 @@ func TestFat32Mkdir(t *testing.T) {
 			}
 		}
 	}
-	t.Run("read to Mkdir", func(t *testing.T) {
+	t.Run("fat32.Read to Mkdir", func(t *testing.T) {
 		t.Run("entire image", func(t *testing.T) {
 			runTest(t, 0, 0, fat32.Read)
 		})
@@ -169,9 +171,9 @@ func TestFat32Mkdir(t *testing.T) {
 			runTest(t, 500, 1000, fat32.Read)
 		})
 	})
-	t.Run("Create to Mkdir", func(t *testing.T) {
+	t.Run("fat32.Create to Mkdir", func(t *testing.T) {
 		// This is to enable Create "fit" into the common testing logic
-		createShim := func(file util.File, size int64, start int64, blocksize int64) (*fat32.FileSystem, error) {
+		createShim := func(file backend.Storage, size int64, start int64, blocksize int64) (*fat32.FileSystem, error) {
 			return fat32.Create(file, size, start, blocksize, "")
 		}
 		t.Run("entire image", func(t *testing.T) {
@@ -207,8 +209,10 @@ func TestFat32Create(t *testing.T) {
 					t.Fatal(err)
 				}
 				defer os.Remove(f.Name())
+
+				b := file.New(f, false)
 				// create the filesystem
-				fs, err := fat32.Create(f, tt.filesize-pre-post, pre, tt.blocksize, "")
+				fs, err := fat32.Create(b, tt.filesize-pre-post, pre, tt.blocksize, "")
 				switch {
 				case (err == nil && tt.err != nil) || (err != nil && tt.err == nil) || (err != nil && tt.err != nil && !strings.HasPrefix(err.Error(), tt.err.Error())):
 					t.Errorf("Create(%s, %d, %d, %d): mismatched errors\nactual %v\nexpected %v", f.Name(), tt.filesize, 0, tt.blocksize, err, tt.err)
@@ -267,8 +271,10 @@ func TestFat32Read(t *testing.T) {
 					_, _ = f.WriteAt(b, tt.bytechange+pre)
 					corrupted = fmt.Sprintf("corrupted %d", tt.bytechange+pre)
 				}
+
+				b := file.New(f, true)
 				// create the filesystem
-				fs, err := fat32.Read(f, tt.filesize-pre-post, pre, tt.blocksize)
+				fs, err := fat32.Read(b, tt.filesize-pre-post, pre, tt.blocksize)
 				switch {
 				case (err == nil && tt.err != nil) || (err != nil && tt.err == nil) || (err != nil && tt.err != nil && !strings.HasPrefix(err.Error(), tt.err.Error())):
 					t.Errorf("read(%s, %d, %d, %d) %s: mismatched errors, actual %v expected %v", f.Name(), tt.filesize, 0, tt.blocksize, corrupted, err, tt.err)
@@ -329,7 +335,9 @@ func TestFat32ReadDir(t *testing.T) {
 		if err != nil {
 			t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 		}
-		fs, err := fat32.Read(f, fileInfo.Size()-pre-post, pre, 512)
+
+		b := file.New(f, true)
+		fs, err := fat32.Read(b, fileInfo.Size()-pre-post, pre, 512)
 		if err != nil {
 			t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 		}
@@ -398,7 +406,9 @@ func TestFat32OpenFile(t *testing.T) {
 			if err != nil {
 				t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 			}
-			fs, err := fat32.Read(f, fileInfo.Size()-pre-post, pre, 512)
+
+			b := file.New(f, true)
+			fs, err := fat32.Read(b, fileInfo.Size()-pre-post, pre, 512)
 			if err != nil {
 				t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 			}
@@ -478,7 +488,7 @@ func TestFat32OpenFile(t *testing.T) {
 					if err != nil {
 						t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 					}
-					fs, err := fat32.Read(f, fileInfo.Size()-pre-post, pre, 512)
+					fs, err := fat32.Read(file.New(f, false), fileInfo.Size()-pre-post, pre, 512)
 					if err != nil {
 						t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 					}
@@ -554,7 +564,8 @@ func TestFat32OpenFile(t *testing.T) {
 			if err != nil {
 				t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 			}
-			fs, err := fat32.Create(f, fileInfo.Size()-pre-post, pre, 512, " NO NAME")
+			backend := file.New(f, false)
+			fs, err := fat32.Create(backend, fileInfo.Size()-pre-post, pre, 512, " NO NAME")
 			if err != nil {
 				t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 			}
@@ -609,23 +620,25 @@ func TestFat32OpenFile(t *testing.T) {
 	t.Run("Large File", func(t *testing.T) {
 		//nolint:thelper // this is not a helper function
 		runTest := func(t *testing.T, pre, post int64) {
-			// get a temporary working file
-			f, err := tmpFat32(true, pre, post)
+			// get a temporary working testFile
+			testFile, err := tmpFat32(true, pre, post)
 			if err != nil {
 				t.Fatal(err)
 			}
+
+			f := file.New(testFile, false)
 			if keepTmpFiles == "" {
-				defer os.Remove(f.Name())
+				defer os.Remove(testFile.Name())
 			} else {
-				fmt.Println(f.Name())
+				fmt.Println(testFile.Name())
 			}
 			fileInfo, err := f.Stat()
 			if err != nil {
-				t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
+				t.Fatalf("error getting file info for tmpfile %s: %v", testFile.Name(), err)
 			}
 			fs, err := fat32.Read(f, fileInfo.Size()-pre-post, pre, 512)
 			if err != nil {
-				t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
+				t.Fatalf("error reading fat32 filesystem from %s: %v", testFile.Name(), err)
 			}
 			path := "/abcdefghi"
 			mode := os.O_RDWR | os.O_CREATE
@@ -683,7 +696,7 @@ func TestFat32OpenFile(t *testing.T) {
 		if err != nil {
 			t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 		}
-		fs, err := fat32.Read(f, fileInfo.Size(), 0, 512)
+		fs, err := fat32.Read(file.New(f, false), fileInfo.Size(), 0, 512)
 		if err != nil {
 			t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 		}
@@ -750,7 +763,7 @@ func TestFat32OpenFile(t *testing.T) {
 			if err != nil {
 				t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 			}
-			fs, err := fat32.Read(f, fileInfo.Size()-pre-post, pre, 512)
+			fs, err := fat32.Read(file.New(f, false), fileInfo.Size()-pre-post, pre, 512)
 			if err != nil {
 				t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 			}
@@ -828,7 +841,7 @@ func TestFat32Label(t *testing.T) {
 		}
 
 		// read the filesystem
-		fs, err := fat32.Read(f, fileInfo.Size(), 0, 512)
+		fs, err := fat32.Read(file.New(f, false), fileInfo.Size(), 0, 512)
 		if err != nil {
 			t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 		}
@@ -858,8 +871,9 @@ func TestFat32Label(t *testing.T) {
 			t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 		}
 
+		theBackend := file.New(f, false)
 		// create an empty filesystem
-		fs, err := fat32.Create(f, fileInfo.Size(), 0, 512, "go-diskfs")
+		fs, err := fat32.Create(theBackend, fileInfo.Size(), 0, 512, "go-diskfs")
 		if err != nil {
 			t.Fatalf("error creating fat32 filesystem: %v", err)
 		}
@@ -879,8 +893,9 @@ func TestFat32Label(t *testing.T) {
 			t.Fatalf("error re-opening file %s: %v", f.Name(), err)
 		}
 
+		theBackend = file.New(f, false)
 		// read the filesystem
-		fs, err = fat32.Read(f, fileInfo.Size(), 0, 512)
+		fs, err = fat32.Read(theBackend, fileInfo.Size(), 0, 512)
 		if err != nil {
 			t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 		}
@@ -910,8 +925,9 @@ func TestFat32Label(t *testing.T) {
 			t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 		}
 
+		theBackend := file.New(f, false)
 		// create an empty filesystem
-		fs, err := fat32.Create(f, fileInfo.Size(), 0, 512, "go-diskfs")
+		fs, err := fat32.Create(theBackend, fileInfo.Size(), 0, 512, "go-diskfs")
 		if err != nil {
 			t.Fatalf("error creating fat32 filesystem: %v", err)
 		}
@@ -937,8 +953,9 @@ func TestFat32Label(t *testing.T) {
 			t.Fatalf("error re-opening file %s: %v", f.Name(), err)
 		}
 
+		theBackend = file.New(f, false)
 		// read the filesystem
-		fs, err = fat32.Read(f, fileInfo.Size(), 0, 512)
+		fs, err = fat32.Read(theBackend, fileInfo.Size(), 0, 512)
 		if err != nil {
 			t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 		}
@@ -957,7 +974,8 @@ func TestFat32MkdirCases(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer os.Remove(f.Name())
-	fs, err := fat32.Create(f, 1048576, 0, 512, "")
+	theBackend := file.New(f, false)
+	fs, err := fat32.Create(theBackend, 1048576, 0, 512, "")
 	if err != nil {
 		t.Error(err.Error())
 	}
@@ -998,7 +1016,7 @@ func Test83Lowercase(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 	}
-	fs, err := fat32.Read(f, fileInfo.Size(), 0, 512)
+	fs, err := fat32.Read(file.New(f, false), fileInfo.Size(), 0, 512)
 	if err != nil {
 		t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 	}
@@ -1036,7 +1054,7 @@ func TestOpenFileCaseInsensitive(t *testing.T) {
 	if err != nil {
 		t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 	}
-	fs, err := fat32.Read(f, fileInfo.Size(), 0, 512)
+	fs, err := fat32.Read(file.New(f, false), fileInfo.Size(), 0, 512)
 	if err != nil {
 		t.Fatalf("error reading fat32 filesystem from %s: %v", f.Name(), err)
 	}
@@ -1055,11 +1073,11 @@ func TestOpenFileCaseInsensitive(t *testing.T) {
 		"/TERCER_ARCHIVO",
 	}
 	for _, path := range paths {
-		file, err := fs.OpenFile(path, os.O_RDONLY)
+		fat32File, err := fs.OpenFile(path, os.O_RDONLY)
 		if err != nil {
 			t.Errorf("error opening %s: %v\n", path, err)
 		} else {
-			file.Close()
+			fat32File.Close()
 		}
 	}
 }
@@ -1084,7 +1102,7 @@ func TestCreateFileTree(t *testing.T) {
 
 	// 6GB to test large disk
 	size := int64(6 * 1024 * 1024 * 1024)
-	d, err := diskfs.Create(tmpImgPath, size, diskfs.Raw, diskfs.SectorSizeDefault)
+	d, err := diskfs.Create(tmpImgPath, size, diskfs.SectorSizeDefault)
 	if err != nil {
 		t.Fatalf("error creating disk: %v", err)
 	}
@@ -1116,32 +1134,32 @@ func TestCreateFileTree(t *testing.T) {
 		if err := fs.Mkdir(blobdir); err != nil {
 			t.Errorf("Error making directory %s: %v", blobdir, err)
 		}
-		file := path.Join(blobdir, "microfile")
-		if err := testMkFile(fs, file, 11); err != nil {
-			t.Errorf("Error making microfile %s: %v", file, err)
+		testFile := path.Join(blobdir, "microfile")
+		if err := testMkFile(fs, testFile, 11); err != nil {
+			t.Errorf("Error making microfile %s: %v", testFile, err)
 		}
-		file = path.Join(blobdir, "randfile")
+		testFile = path.Join(blobdir, "randfile")
 		size := mathrandv2.IntN(73) // #nosec G404
-		if err := testMkFile(fs, file, size); err != nil {
-			t.Errorf("Error making random file %s: %v", file, err)
+		if err := testMkFile(fs, testFile, size); err != nil {
+			t.Errorf("Error making random file %s: %v", testFile, err)
 		}
-		file = path.Join(blobdir, "smallfile")
-		if err := testMkFile(fs, file, 5*1024*1024); err != nil {
-			t.Errorf("Error making small file %s: %v", file, err)
+		testFile = path.Join(blobdir, "smallfile")
+		if err := testMkFile(fs, testFile, 5*1024*1024); err != nil {
+			t.Errorf("Error making small file %s: %v", testFile, err)
 		}
 	}
-	file := "/b/sub49/blob/gigfile1"
+	testFile := "/b/sub49/blob/gigfile1"
 	gb := 1024 * 1024 * 1024
-	if err := testMkFile(fs, file, gb); err != nil {
-		t.Errorf("Error making gigfile1 %s: %v", file, err)
+	if err := testMkFile(fs, testFile, gb); err != nil {
+		t.Errorf("Error making gigfile1 %s: %v", testFile, err)
 	}
-	file = "/b/sub50/blob/gigfile1"
-	if err := testMkFile(fs, file, gb); err != nil {
-		t.Errorf("Error making gigfile1 %s: %v", file, err)
+	testFile = "/b/sub50/blob/gigfile1"
+	if err := testMkFile(fs, testFile, gb); err != nil {
+		t.Errorf("Error making gigfile1 %s: %v", testFile, err)
 	}
-	file = "/b/sub51/blob/gigfile1"
-	if err := testMkFile(fs, file, gb); err != nil {
-		t.Errorf("Error making gigfile1 %s: %v", file, err)
+	testFile = "/b/sub51/blob/gigfile1"
+	if err := testMkFile(fs, testFile, gb); err != nil {
+		t.Errorf("Error making gigfile1 %s: %v", testFile, err)
 	}
 }
 
@@ -1314,8 +1332,9 @@ func Test_Rename(t *testing.T) {
 				t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 			}
 
+			b := file.New(f, false)
 			// create an empty filesystem
-			fs, err := fat32.Create(f, fileInfo.Size(), 0, 512, "go-diskfs")
+			fs, err := fat32.Create(b, fileInfo.Size(), 0, 512, "go-diskfs")
 			if err != nil {
 				t.Fatalf("error creating fat32 filesystem: %v", err)
 			}
@@ -1473,8 +1492,9 @@ func Test_Remove(t *testing.T) {
 				t.Fatalf("error getting file info for tmpfile %s: %v", f.Name(), err)
 			}
 
+			b := file.New(f, false)
 			// create an empty filesystem
-			fs, err := fat32.Create(f, fileInfo.Size(), 0, 512, "go-diskfs")
+			fs, err := fat32.Create(b, fileInfo.Size(), 0, 512, "go-diskfs")
 			if err != nil {
 				t.Fatalf("error creating fat32 filesystem: %v", err)
 			}
diff --git a/filesystem/fat32/testdata/fat32.go b/filesystem/fat32/testdata/fat32.go
index 68c024d..ee3005f 100644
--- a/filesystem/fat32/testdata/fat32.go
+++ b/filesystem/fat32/testdata/fat32.go
@@ -20,7 +20,7 @@ func main() {
 }
 func mkfs(name string) filesystem.FileSystem {
 	size := int64(10 * 1024 * 1024)
-	d, err := diskfs.Create(name, size, diskfs.Raw, diskfs.SectorSizeDefault)
+	d, err := diskfs.Create(name, size, diskfs.SectorSizeDefault)
 	if err != nil {
 		fmt.Printf("error creating disk: %v", err)
 		os.Exit(1)
diff --git a/filesystem/iso9660/common_internal_test.go b/filesystem/iso9660/common_internal_test.go
index e9e6424..4e2b2a7 100644
--- a/filesystem/iso9660/common_internal_test.go
+++ b/filesystem/iso9660/common_internal_test.go
@@ -4,6 +4,8 @@ import (
 	"os"
 	"testing"
 	"time"
+
+	"github.com/diskfs/go-diskfs/backend/file"
 )
 
 const (
@@ -13,7 +15,7 @@ const (
 	ISO9660Size   = 11018240
 )
 
-func GetTestFile(t *testing.T) (file *File, name string) {
+func GetTestFile(t *testing.T) (isoFile *File, name string) {
 	t.Helper()
 	// we use the entry for FILENA01.;1 , which should have the content "filename_01" (without the quotes)
 	// see ./testdata/README.md
@@ -29,7 +31,7 @@ func GetTestFile(t *testing.T) (file *File, name string) {
 		workspace: "",
 		size:      ISO9660Size,
 		start:     0,
-		file:      f,
+		backend:   file.New(f, true),
 		blocksize: 2048,
 	}
 	de := &directoryEntry{
@@ -48,10 +50,10 @@ func GetTestFile(t *testing.T) (file *File, name string) {
 	}, "README\n"
 }
 
-func GetLargeTestFile(t *testing.T) (file *File, size uint32) {
+func GetLargeTestFile(t *testing.T) (isoFile *File, size uint32) {
 	t.Helper()
 	// FileSystem implements the FileSystem interface
-	f, err := os.Open(ISO9660File)
+	testFile, err := os.Open(ISO9660File)
 	if err != nil {
 		t.Errorf("could not read ISO9660 test file %s: %v", ISO9660File, err)
 	}
@@ -59,7 +61,7 @@ func GetLargeTestFile(t *testing.T) (file *File, size uint32) {
 		workspace: "",
 		size:      ISO9660Size,
 		start:     0,
-		file:      f,
+		backend:   file.New(testFile, true),
 		blocksize: 2048,
 	}
 	de := &directoryEntry{
diff --git a/filesystem/iso9660/compatibility_test.go b/filesystem/iso9660/compatibility_test.go
index c9ccf50..488b8a8 100644
--- a/filesystem/iso9660/compatibility_test.go
+++ b/filesystem/iso9660/compatibility_test.go
@@ -4,6 +4,7 @@ import (
 	"os"
 	"testing"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/filesystem"
 )
 
@@ -13,7 +14,9 @@ func TestISO9660FSCompatibility(t *testing.T) {
 		t.Fatalf("Failed to read iso9660 testfile: %v", err)
 	}
 	defer f.Close()
-	isofs, err := Read(f, 0, 0, 2048)
+
+	b := file.New(f, true)
+	isofs, err := Read(b, 0, 0, 2048)
 	if err != nil {
 		t.Fatalf("iso read: %s", err)
 	}
diff --git a/filesystem/iso9660/directory_internal_test.go b/filesystem/iso9660/directory_internal_test.go
index 480be10..26819d8 100644
--- a/filesystem/iso9660/directory_internal_test.go
+++ b/filesystem/iso9660/directory_internal_test.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/testhelper"
 )
 
@@ -29,7 +30,7 @@ func TestDirectoryEntriesFromBytes(t *testing.T) {
 			return 0, fmt.Errorf("unknown area to read %d", offset)
 		},
 	}
-	fs.file = f
+	fs.backend = file.New(f, true)
 
 	d := &Directory{}
 
diff --git a/filesystem/iso9660/directoryentry_internal_test.go b/filesystem/iso9660/directoryentry_internal_test.go
index cc6de23..3335164 100644
--- a/filesystem/iso9660/directoryentry_internal_test.go
+++ b/filesystem/iso9660/directoryentry_internal_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/testhelper"
 	"github.com/go-test/deep"
 )
@@ -757,7 +758,7 @@ func TestDirectoryEntryParseDirEntries(t *testing.T) {
 			return 0, fmt.Errorf("unknown area to read %d", offset)
 		},
 	}
-	fs.file = f
+	fs.backend = file.New(f, true)
 	tests := []struct {
 		de  []*directoryEntry
 		b   []byte
@@ -855,7 +856,7 @@ func TestDirectoryEntryGetLocation(t *testing.T) {
 		hasMoreEntries:           false,
 		volumeSequence:           1,
 		filename:                 fmt.Sprintf("%x", 0x00),
-		filesystem:               &FileSystem{blocksize: 2048, file: f},
+		filesystem:               &FileSystem{blocksize: 2048, backend: file.New(f, true)},
 	}
 
 	for _, tt := range tests {
diff --git a/filesystem/iso9660/finalize_test.go b/filesystem/iso9660/finalize_test.go
index 23bb3f6..408221f 100644
--- a/filesystem/iso9660/finalize_test.go
+++ b/filesystem/iso9660/finalize_test.go
@@ -10,6 +10,7 @@ import (
 	"regexp"
 	"testing"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/filesystem/iso9660"
 	"github.com/diskfs/go-diskfs/partition/mbr"
@@ -28,7 +29,9 @@ func TestFinalizeElTorito(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Failed to create tmpfile: %v", err)
 	}
-	fs, err := iso9660.Create(f, 0, 0, blocksize, "")
+
+	b := file.New(f, false)
+	fs, err := iso9660.Create(b, 0, 0, blocksize, "")
 	if err != nil {
 		t.Fatalf("Failed to iso9660.Create: %v", err)
 	}
@@ -70,7 +73,7 @@ func TestFinalizeElTorito(t *testing.T) {
 	}
 
 	// now check the contents
-	fs, err = iso9660.Read(f, 0, 0, 2048)
+	fs, err = iso9660.Read(b, 0, 0, 2048)
 	if err != nil {
 		t.Fatalf("error reading the tmpfile as iso: %v", err)
 	}
@@ -107,7 +110,9 @@ func TestFinalize9660(t *testing.T) {
 		if err != nil {
 			t.Fatalf("Failed to create tmpfile: %v", err)
 		}
-		fs, err := iso9660.Create(f, 0, 0, blocksize, "")
+
+		b := file.New(f, false)
+		fs, err := iso9660.Create(b, 0, 0, blocksize, "")
 		if err != nil {
 			t.Fatalf("Failed to iso9660.Create: %v", err)
 		}
@@ -129,7 +134,9 @@ func TestFinalize9660(t *testing.T) {
 		if err != nil {
 			t.Fatalf("Failed to create tmpfile: %v", err)
 		}
-		fs, err := iso9660.Create(f, 0, 0, blocksize, "")
+
+		b := file.New(f, false)
+		fs, err := iso9660.Create(b, 0, 0, blocksize, "")
 		if err != nil {
 			t.Fatalf("Failed to iso9660.Create: %v", err)
 		}
@@ -163,9 +170,9 @@ func TestFinalize9660(t *testing.T) {
 		if err != nil {
 			t.Fatalf("Failed to iso9660.OpenFile(%s): %v", "README.MD", err)
 		}
-		b := []byte("readme\n")
-		if _, err = isofile.Write(b); err != nil {
-			t.Fatalf("error writing %s to tmpfile %s: %v", string(b), "README.MD", err)
+		dataBytes := []byte("readme\n")
+		if _, err = isofile.Write(dataBytes); err != nil {
+			t.Fatalf("error writing %s to tmpfile %s: %v", string(dataBytes), "README.MD", err)
 		}
 
 		fooCount := 75
@@ -196,7 +203,7 @@ func TestFinalize9660(t *testing.T) {
 		}
 
 		// now check the contents
-		fs, err = iso9660.Read(f, 0, 0, 2048)
+		fs, err = iso9660.Read(b, 0, 0, 2048)
 		if err != nil {
 			t.Fatalf("error reading the tmpfile as iso: %v", err)
 		}
@@ -285,7 +292,9 @@ func TestFinalize9660(t *testing.T) {
 		if err != nil {
 			t.Fatalf("Failed to create tmpfile: %v", err)
 		}
-		fs, err := iso9660.Create(f, 0, 0, blocksize, dir)
+
+		b := file.New(f, false)
+		fs, err := iso9660.Create(b, 0, 0, blocksize, dir)
 		if err != nil {
 			t.Fatalf("Failed to iso9660.Create: %v", err)
 		}
@@ -295,7 +304,7 @@ func TestFinalize9660(t *testing.T) {
 		}
 
 		// now check the contents
-		fs, err = iso9660.Read(f, 0, 0, 2048)
+		fs, err = iso9660.Read(b, 0, 0, 2048)
 		if err != nil {
 			t.Fatalf("error reading the tmpfile as iso: %v", err)
 		}
@@ -344,7 +353,9 @@ func TestFinalizeRockRidge(t *testing.T) {
 		if err != nil {
 			t.Fatalf("Failed to create tmpfile: %v", err)
 		}
-		fs, err := iso9660.Create(f, 0, 0, blocksize, "")
+
+		b := file.New(f, false)
+		fs, err := iso9660.Create(b, 0, 0, blocksize, "")
 		if err != nil {
 			t.Fatalf("Failed to iso9660.Create: %v", err)
 		}
@@ -384,9 +395,9 @@ func TestFinalizeRockRidge(t *testing.T) {
 		if err != nil {
 			t.Fatalf("Failed to iso9660.OpenFile(%s): %v", "README.md", err)
 		}
-		b := []byte("readme\n")
-		if _, err = isofile.Write(b); err != nil {
-			t.Fatalf("error writing %s to tmpfile %s: %v", string(b), "README.md", err)
+		dataBytes := []byte("readme\n")
+		if _, err = isofile.Write(dataBytes); err != nil {
+			t.Fatalf("error writing %s to tmpfile %s: %v", string(dataBytes), "README.md", err)
 		}
 
 		fooCount := 75
@@ -424,7 +435,7 @@ func TestFinalizeRockRidge(t *testing.T) {
 		}
 
 		// now check the contents
-		fs, err = iso9660.Read(f, 0, 0, 2048)
+		fs, err = iso9660.Read(b, 0, 0, 2048)
 		if err != nil {
 			t.Fatalf("error reading the tmpfile as iso: %v", err)
 		}
diff --git a/filesystem/iso9660/iso9660_internal_test.go b/filesystem/iso9660/iso9660_internal_test.go
index efb29e5..f2ea52b 100644
--- a/filesystem/iso9660/iso9660_internal_test.go
+++ b/filesystem/iso9660/iso9660_internal_test.go
@@ -3,17 +3,19 @@ package iso9660
 import (
 	"os"
 	"testing"
+
+	"github.com/diskfs/go-diskfs/backend/file"
 )
 
 func TestIso9660ReadDirectory(t *testing.T) {
-	// will use the file.iso fixture to test an actual directory
+	// will use the testFile.iso fixture to test an actual directory
 	// \ (root directory) should be in one block
 	// \FOO should be in multiple blocks
-	file, err := os.Open(ISO9660File)
+	testFile, err := os.Open(ISO9660File)
 	if err != nil {
 		t.Fatalf("could not open file %s to read: %v", ISO9660File, err)
 	}
-	defer file.Close()
+	defer testFile.Close()
 	// FileSystem implements the FileSystem interface
 	pathTable, _, _, err := get9660PathTable()
 	if err != nil {
@@ -23,7 +25,7 @@ func TestIso9660ReadDirectory(t *testing.T) {
 		workspace: "", // we only ever call readDirectory with no workspace
 		size:      ISO9660Size,
 		start:     0,
-		file:      file,
+		backend:   file.New(testFile, true),
 		blocksize: 2048,
 		pathTable: pathTable,
 	}
@@ -66,14 +68,14 @@ func TestIso9660ReadDirectory(t *testing.T) {
 }
 
 func TestRockRidgeReadDirectory(t *testing.T) {
-	// will use the file.iso fixture to test an actual directory
+	// will use the testFile.iso fixture to test an actual directory
 	// \ (root directory) should be in one block
 	// \FOO should be in multiple blocks
-	file, err := os.Open(RockRidgeFile)
+	testFile, err := os.Open(RockRidgeFile)
 	if err != nil {
 		t.Fatalf("could not open file %s to read: %v", RockRidgeFile, err)
 	}
-	defer file.Close()
+	defer testFile.Close()
 	// FileSystem implements the FileSystem interface
 	pathTable, _, _, err := getRockRidgePathTable()
 	if err != nil {
@@ -83,7 +85,7 @@ func TestRockRidgeReadDirectory(t *testing.T) {
 		workspace:      "", // we only ever call readDirectory with no workspace
 		size:           ISO9660Size,
 		start:          0,
-		file:           file,
+		backend:        file.New(testFile, true),
 		blocksize:      2048,
 		pathTable:      pathTable,
 		suspEnabled:    true,
diff --git a/filesystem/iso9660/iso9660_test.go b/filesystem/iso9660/iso9660_test.go
index a0b1633..ee0af59 100644
--- a/filesystem/iso9660/iso9660_test.go
+++ b/filesystem/iso9660/iso9660_test.go
@@ -15,6 +15,7 @@ import (
 	"testing"
 
 	diskfs "github.com/diskfs/go-diskfs"
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/disk"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/filesystem/iso9660"
@@ -42,7 +43,9 @@ func getValidIso9660FSWorkspace() (*iso9660.FileSystem, error) {
 	if err != nil {
 		return nil, fmt.Errorf("Failed to create iso9660 tmpfile: %v", err)
 	}
-	return iso9660.Create(f, 0, 0, 2048, "")
+
+	b := file.New(f, false)
+	return iso9660.Create(b, 0, 0, 2048, "")
 }
 func getValidIso9660FSUserWorkspace() (*iso9660.FileSystem, error) {
 	f, err := tmpIso9660File()
@@ -53,21 +56,26 @@ func getValidIso9660FSUserWorkspace() (*iso9660.FileSystem, error) {
 	if err != nil {
 		return nil, fmt.Errorf("Failed to create iso9660 tmpfile: %v", err)
 	}
-	return iso9660.Create(f, 0, 0, 2048, dir)
+
+	b := file.New(f, false)
+	return iso9660.Create(b, 0, 0, 2048, dir)
 }
 func getValidIso9660FSReadOnly() (*iso9660.FileSystem, error) {
 	f, err := os.Open(iso9660.ISO9660File)
 	if err != nil {
 		return nil, fmt.Errorf("Failed to read iso9660 testfile %s: %v", iso9660.ISO9660File, err)
 	}
-	return iso9660.Read(f, 0, 0, 2048)
+
+	b := file.New(f, true)
+	return iso9660.Read(b, 0, 0, 2048)
 }
 func getValidRockRidgeFSReadOnly() (*iso9660.FileSystem, error) {
 	f, err := os.Open(iso9660.RockRidgeFile)
 	if err != nil {
 		return nil, fmt.Errorf("Failed to read iso9660 testfile %s: %v", iso9660.RockRidgeFile, err)
 	}
-	return iso9660.Read(f, 0, 0, 2048)
+	b := file.New(f, true)
+	return iso9660.Read(b, 0, 0, 2048)
 }
 
 func tmpIso9660File() (*os.File, error) {
@@ -182,7 +190,9 @@ func TestIso9660Create(t *testing.T) {
 			if err != nil {
 				t.Errorf("Failed to create iso9660 tmpfile: %v", err)
 			}
-			fs, err := iso9660.Create(f, tt.filesize, 0, tt.blocksize, tt.workdir)
+
+			b := file.New(f, false)
+			fs, err := iso9660.Create(b, tt.filesize, 0, tt.blocksize, tt.workdir)
 			defer os.Remove(f.Name())
 			switch {
 			case (err == nil && tt.err != nil) || (err != nil && tt.err == nil) || (err != nil && tt.err != nil && !strings.HasPrefix(err.Error(), tt.err.Error())):
@@ -223,8 +233,10 @@ func TestISO9660Read(t *testing.T) {
 				t.Fatal(err)
 			}
 			defer f.Close()
+
+			b := file.New(f, true)
 			// create the filesystem
-			fs, err := iso9660.Read(f, tt.filesize, 0, tt.blocksize)
+			fs, err := iso9660.Read(b, tt.filesize, 0, tt.blocksize)
 			switch {
 			case (err == nil && tt.err != nil) || (err != nil && tt.err == nil) || (err != nil && tt.err != nil && !strings.HasPrefix(err.Error(), tt.err.Error())):
 				t.Errorf("read(%s, %d, %d, %d): mismatched errors, actual %v expected %v", f.Name(), tt.filesize, 0, tt.blocksize, err, tt.err)
@@ -545,7 +557,7 @@ func TestIso9660Finalize(t *testing.T) {
 
 		// Create the disk image
 		// TODO: Explain why we need to use Raw here
-		mydisk, err := diskfs.Create(outputFileName, 100*1024, diskfs.Raw, LogicalBlocksize)
+		mydisk, err := diskfs.Create(outputFileName, 100*1024, LogicalBlocksize)
 		if err != nil {
 			return err
 		}
diff --git a/filesystem/squashfs/const_internal_test.go b/filesystem/squashfs/const_internal_test.go
index 8601257..cb74112 100644
--- a/filesystem/squashfs/const_internal_test.go
+++ b/filesystem/squashfs/const_internal_test.go
@@ -7,10 +7,11 @@ package squashfs
 // 3. Take the relevant sizes, locations and inodes and use them here.
 
 import (
+	"io/fs"
 	"os"
 	"time"
 
-	"github.com/diskfs/go-diskfs/util"
+	"github.com/diskfs/go-diskfs/backend/file"
 )
 
 const (
@@ -143,14 +144,13 @@ var (
 	testLargeDirEntryCount = 252
 )
 
-func testGetFilesystem(f util.File) (*FileSystem, []byte, error) {
-	file := f
+func testGetFilesystem(f fs.File) (*FileSystem, []byte, error) {
 	var (
 		err error
 		b   []byte
 	)
-	if file == nil {
-		file, err = os.Open(SquashfsUncompressedfile)
+	if f == nil {
+		f, err = os.Open(SquashfsUncompressedfile)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -161,7 +161,7 @@ func testGetFilesystem(f util.File) (*FileSystem, []byte, error) {
 	}
 	blocksize := int64(testValidBlocksize)
 	sb := testValidSuperblockUncompressed
-	fs := &FileSystem{
+	testFs := &FileSystem{
 		/*
 			TODO: Still need to add these in
 			uidsGids       []byte
@@ -173,7 +173,7 @@ func testGetFilesystem(f util.File) (*FileSystem, []byte, error) {
 		compressor: &CompressorGzip{},
 		size:       5251072,
 		start:      0,
-		file:       file,
+		backend:    file.New(f, true),
 		blocksize:  blocksize,
 		xattrs:     nil,
 		rootDir: &inodeImpl{
@@ -196,15 +196,15 @@ func testGetFilesystem(f util.File) (*FileSystem, []byte, error) {
 		uidsGids:   []uint32{5, 0, 1},
 		superblock: sb,
 	}
-	return fs, b, nil
+	return testFs, b, nil
 }
 
 func testGetInodeMetabytes() (inodeBytes []byte, inodesStart uint64, err error) {
-	fs, b, err := testGetFilesystem(nil)
+	testFs, b, err := testGetFilesystem(nil)
 	if err != nil {
 		return nil, 0, err
 	}
-	return b[fs.superblock.inodeTableStart+2:], fs.superblock.inodeTableStart, nil
+	return b[testFs.superblock.inodeTableStart+2:], testFs.superblock.inodeTableStart, nil
 }
 
 //nolint:deadcode // we need these references in the future
@@ -233,12 +233,12 @@ func testGetFilesystemRoot() []*directoryEntry {
 }
 
 // GetTestFileSmall get a *squashfs.File to a usable and known test file
-func GetTestFileSmall(f util.File, c Compressor) (*File, error) {
-	fs, _, err := testGetFilesystem(f)
+func GetTestFileSmall(f fs.File, c Compressor) (*File, error) {
+	testFs, _, err := testGetFilesystem(f)
 	if err != nil {
 		return nil, err
 	}
-	fs.compressor = c
+	testFs.compressor = c
 	ef := &extendedFile{
 		startBlock:         superblockSize,
 		fileSize:           7,
@@ -255,19 +255,19 @@ func GetTestFileSmall(f util.File, c Compressor) (*File, error) {
 		isReadWrite:  false,
 		isAppend:     false,
 		offset:       0,
-		filesystem:   fs,
+		filesystem:   testFs,
 	}, nil
 }
 
 // GetTestFileBig get a *squashfs.File to a usable and known test file
-func GetTestFileBig(f util.File, c Compressor) (*File, error) {
-	fs, _, err := testGetFilesystem(f)
+func GetTestFileBig(f fs.File, c Compressor) (*File, error) {
+	testFs, _, err := testGetFilesystem(f)
 	if err != nil {
 		return nil, err
 	}
-	fs.compressor = c
+	testFs.compressor = c
 	fragSize := uint64(5)
-	size := uint64(fs.blocksize) + fragSize
+	size := uint64(testFs.blocksize) + fragSize
 	ef := &extendedFile{
 		startBlock:         superblockSize,
 		fileSize:           size,
@@ -277,7 +277,7 @@ func GetTestFileBig(f util.File, c Compressor) (*File, error) {
 		fragmentOffset:     7,
 		xAttrIndex:         0,
 		blockSizes: []*blockData{
-			{size: uint32(fs.blocksize), compressed: false},
+			{size: uint32(testFs.blocksize), compressed: false},
 		},
 	}
 	// inode 0, offset 0, name "README.md", type basic file
@@ -286,6 +286,6 @@ func GetTestFileBig(f util.File, c Compressor) (*File, error) {
 		isReadWrite:  false,
 		isAppend:     false,
 		offset:       0,
-		filesystem:   fs,
+		filesystem:   testFs,
 	}, nil
 }
diff --git a/filesystem/squashfs/finalize_test.go b/filesystem/squashfs/finalize_test.go
index 201a4fa..82d0ebd 100644
--- a/filesystem/squashfs/finalize_test.go
+++ b/filesystem/squashfs/finalize_test.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"testing"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/filesystem/squashfs"
 	"github.com/diskfs/go-diskfs/testhelper"
@@ -27,7 +28,9 @@ func TestFinalizeSquashfs(t *testing.T) {
 		if err != nil {
 			t.Fatalf("Failed to create tmpfile: %v", err)
 		}
-		fs, err := squashfs.Create(f, 0, 0, blocksize)
+
+		b := file.New(f, false)
+		fs, err := squashfs.Create(b, 0, 0, blocksize)
 		if err != nil {
 			t.Fatalf("Failed to squashfs.Create: %v", err)
 		}
@@ -61,9 +64,9 @@ func TestFinalizeSquashfs(t *testing.T) {
 		if err != nil {
 			t.Fatalf("Failed to squashfs.OpenFile(%s): %v", "README.MD", err)
 		}
-		b := []byte("readme\n")
-		if _, err = sqsfile.Write(b); err != nil {
-			t.Fatalf("error writing %s to tmpfile %s: %v", string(b), "README.MD", err)
+		dataBytes := []byte("readme\n")
+		if _, err = sqsfile.Write(dataBytes); err != nil {
+			t.Fatalf("error writing %s to tmpfile %s: %v", string(dataBytes), "README.MD", err)
 		}
 
 		fooCount := 75
@@ -94,7 +97,7 @@ func TestFinalizeSquashfs(t *testing.T) {
 		}
 
 		// now check the contents
-		fs, err = squashfs.Read(f, 0, 0, blocksize)
+		fs, err = squashfs.Read(b, 0, 0, blocksize)
 		if err != nil {
 			t.Fatalf("error reading the tmpfile as squashfs: %v", err)
 		}
diff --git a/filesystem/squashfs/squashfs_internal_test.go b/filesystem/squashfs/squashfs_internal_test.go
index 6c19779..d5109dc 100644
--- a/filesystem/squashfs/squashfs_internal_test.go
+++ b/filesystem/squashfs/squashfs_internal_test.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/testhelper"
 )
@@ -130,7 +131,7 @@ func TestReadXAttrsTable(t *testing.T) {
 	indexBody := make([]byte, 8)
 	binary.LittleEndian.PutUint64(indexBody, uint64(xAttrIDStart))
 
-	file := &testhelper.FileImpl{
+	testFile := &testhelper.FileImpl{
 		Reader: func(b []byte, offset int64) (int, error) {
 			var b2 []byte
 			switch offset {
@@ -159,7 +160,7 @@ func TestReadXAttrsTable(t *testing.T) {
 		data: table[2:],
 		list: list,
 	}
-	xtable, err := readXattrsTable(s, file, nil)
+	xtable, err := readXattrsTable(s, testFile, nil)
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
@@ -182,7 +183,7 @@ func TestReadFragmentTable(t *testing.T) {
 	if err != nil {
 		t.Fatalf("unable to read test file: %v", err)
 	}
-	entries, err := readFragmentTable(fs.superblock, fs.file, fs.compressor)
+	entries, err := readFragmentTable(fs.superblock, fs.backend, fs.compressor)
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
@@ -239,7 +240,7 @@ func TestReadBlock(t *testing.T) {
 	smallLocation := int64(2000)
 	size := uint32(20)
 	data := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
-	file := &testhelper.FileImpl{
+	testFile := &testhelper.FileImpl{
 		Reader: func(b []byte, offset int64) (int, error) {
 			switch {
 			case offset == location:
@@ -273,7 +274,7 @@ func TestReadBlock(t *testing.T) {
 	}
 	for i, tt := range tests {
 		fs := &FileSystem{
-			file:       file,
+			backend:    file.New(testFile, true),
 			compressor: tt.compressor,
 		}
 		b, err := fs.readBlock(tt.location, tt.compressed, size)
@@ -300,7 +301,7 @@ func TestReadFragment(t *testing.T) {
 	data := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
 		20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
 		30, 31, 32, 33, 34, 35, 36, 37, 38, 39}
-	file := &testhelper.FileImpl{
+	testFile := &testhelper.FileImpl{
 		Reader: func(b []byte, offset int64) (int, error) {
 			for _, f := range fragments {
 				if uint64(offset) == f.start {
@@ -330,7 +331,7 @@ func TestReadFragment(t *testing.T) {
 	for i, tt := range tests {
 		fs := &FileSystem{
 			fragments:  fragments,
-			file:       file,
+			backend:    file.New(testFile, true),
 			compressor: tt.compressor,
 		}
 		b, err := fs.readFragment(tt.index, tt.offset, tt.size)
@@ -348,7 +349,7 @@ func TestReadFragment(t *testing.T) {
 }
 
 func TestReadUidsGids(t *testing.T) {
-	// func readUidsGids(s *superblock, file util.File, c compressor) ([]uint32, error) {
+	// func readUidsGids(s *superblock, file backend.File, c compressor) ([]uint32, error) {
 	expected := []uint32{
 		0, 10, 100, 1000,
 	}
@@ -367,7 +368,7 @@ func TestReadUidsGids(t *testing.T) {
 		idTableStart: indexStart,
 		idCount:      uint16(len(ids)-2) / 4,
 	}
-	file := &testhelper.FileImpl{
+	testFile := &testhelper.FileImpl{
 		Reader: func(b []byte, offset int64) (int, error) {
 			switch uint64(offset) {
 			case idStart:
@@ -384,7 +385,7 @@ func TestReadUidsGids(t *testing.T) {
 			}
 		},
 	}
-	uidsgids, err := readUidsGids(s, file, nil)
+	uidsgids, err := readUidsGids(s, testFile, nil)
 	switch {
 	case err != nil:
 		t.Errorf("unexpected error: %v", err)
diff --git a/filesystem/squashfs/squashfs_test.go b/filesystem/squashfs/squashfs_test.go
index b58b217..950a5f2 100644
--- a/filesystem/squashfs/squashfs_test.go
+++ b/filesystem/squashfs/squashfs_test.go
@@ -13,6 +13,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/diskfs/go-diskfs/backend/file"
 	"github.com/diskfs/go-diskfs/filesystem"
 	"github.com/diskfs/go-diskfs/filesystem/squashfs"
 )
@@ -48,7 +49,9 @@ func getValidSquashfsFSWorkspace() (*squashfs.FileSystem, error) {
 	if err != nil {
 		return nil, fmt.Errorf("Failed to create squashfs tmpfile: %v", err)
 	}
-	return squashfs.Create(f, 0, 0, 4096)
+
+	b := file.New(f, false)
+	return squashfs.Create(b, 0, 0, 4096)
 }
 
 func getValidSquashfsFSReadOnly() (*squashfs.FileSystem, error) {
@@ -56,7 +59,9 @@ func getValidSquashfsFSReadOnly() (*squashfs.FileSystem, error) {
 	if err != nil {
 		return nil, fmt.Errorf("Failed to read squashfs testfile %s: %v", squashfs.Squashfsfile, err)
 	}
-	return squashfs.Read(f, 0, 0, 4096)
+
+	b := file.New(f, true)
+	return squashfs.Read(b, 0, 0, 4096)
 }
 
 func TestSquashfsType(t *testing.T) {
@@ -358,8 +363,10 @@ func TestSquashfsRead(t *testing.T) {
 				t.Fatal(err)
 			}
 			defer f.Close()
+
+			b := file.New(f, true)
 			// create the filesystem
-			fs, err := squashfs.Read(f, tt.filesize, 0, tt.blocksize)
+			fs, err := squashfs.Read(b, tt.filesize, 0, tt.blocksize)
 			switch {
 			case (err == nil && tt.err != nil) || (err != nil && tt.err == nil) || (err != nil && tt.err != nil && !strings.HasPrefix(err.Error(), tt.err.Error())):
 				t.Errorf("%d: Read(%s, %d, %d, %d): mismatched errors, actual %v expected %v", i, f.Name(), tt.filesize, 0, tt.blocksize, err, tt.err)
@@ -405,8 +412,10 @@ func TestSquashfsCheckListing(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+
+	b := file.New(f, true)
 	// create the filesystem
-	fs, err := squashfs.Read(f, fi.Size(), 0, 0)
+	fs, err := squashfs.Read(b, fi.Size(), 0, 0)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -593,8 +602,10 @@ func TestSquashfsReadFile(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+
+	b := file.New(f, true)
 	// create the filesystem
-	fs, err := squashfs.Read(f, fi.Size(), 0, 0)
+	fs, err := squashfs.Read(b, fi.Size(), 0, 0)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -649,7 +660,9 @@ func TestSquashfsCreate(t *testing.T) {
 			if err != nil {
 				t.Errorf("Failed to create squashfs tmpfile: %v", err)
 			}
-			fs, err := squashfs.Create(f, tt.filesize, 0, tt.blocksize)
+
+			b := file.New(f, false)
+			fs, err := squashfs.Create(b, tt.filesize, 0, tt.blocksize)
 			defer os.Remove(f.Name())
 			switch {
 			case (err == nil && tt.err != nil) || (err != nil && tt.err == nil) || (err != nil && tt.err != nil && !strings.HasPrefix(err.Error(), tt.err.Error())):
@@ -727,8 +740,10 @@ func TestSquashfsReadDirCornerCases(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+
+	b := file.New(f, true)
 	// create the filesystem
-	fs, err := squashfs.Read(f, fi.Size(), 0, 0)
+	fs, err := squashfs.Read(b, fi.Size(), 0, 0)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/partition/gpt/table_internal_test.go b/partition/gpt/table_internal_test.go
index 6a3de79..1a3e86c 100644
--- a/partition/gpt/table_internal_test.go
+++ b/partition/gpt/table_internal_test.go
@@ -197,6 +197,17 @@ func (b *byteBufferReader) Seek(offset int64, whence int) (int64, error) {
 	}
 	return int64(b.pos), nil
 }
+func (b *byteBufferReader) Stat() (os.FileInfo, error) {
+	return nil, nil
+}
+
+func (b *byteBufferReader) Read(p []byte) (int, error) {
+	return b.ReadAt(p, 0)
+}
+
+func (b *byteBufferReader) Close() error {
+	return nil
+}
 
 func TestRead(t *testing.T) {
 	t.Run("invalid EFI Partition Checksum", func(t *testing.T) {
diff --git a/testhelper/fileimpl.go b/testhelper/fileimpl.go
index b593aed..c67010d 100644
--- a/testhelper/fileimpl.go
+++ b/testhelper/fileimpl.go
@@ -1,6 +1,9 @@
 package testhelper
 
-import "fmt"
+import (
+	"fmt"
+	"os"
+)
 
 type reader func(b []byte, offset int64) (int, error)
 type writer func(b []byte, offset int64) (int, error)
@@ -12,6 +15,18 @@ type FileImpl struct {
 	Writer writer
 }
 
+func (f *FileImpl) Stat() (os.FileInfo, error) {
+	return nil, nil
+}
+
+func (f *FileImpl) Read(b []byte) (int, error) {
+	return f.Reader(b, 0)
+}
+
+func (f *FileImpl) Close() error {
+	return nil
+}
+
 // ReadAt read at a particular offset
 func (f *FileImpl) ReadAt(b []byte, offset int64) (int, error) {
 	return f.Reader(b, offset)