Skip to content

Commit

Permalink
Implement file storage abstraction to allow using any fs.File as a st…
Browse files Browse the repository at this point in the history
…orage

Separate interfaces for Read and Write storage operations

Introduce diskfs.OpenStorage(fs.File,...) to newly implemented features.
  • Loading branch information
aol-nnov committed Nov 19, 2024
1 parent 15ebb7b commit d44b7c9
Show file tree
Hide file tree
Showing 48 changed files with 759 additions and 421 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (see a bit stale qcow2 branch).

Currently there is only one implementation - raw.

Use `raw` 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:

Expand Down
33 changes: 33 additions & 0 deletions backend/interface.go
Original file line number Diff line number Diff line change
@@ -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-stecific file for ioctl calls via fd
Sys() (*os.File, error)
// file for read-write operations
Writable() (WritableFile, error)
}
127 changes: 127 additions & 0 deletions backend/raw/raw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package raw

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, isReadonly bool) backend.Storage {
return rawBackend{
storage: f,
readonly: isReadonly,
}
}

// 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, isReadonly 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 !isReadonly {
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: isReadonly,
}, 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-stecific 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
}
69 changes: 31 additions & 38 deletions disk/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -162,9 +151,13 @@ func (d *Disk) CreateFilesystem(spec FilesystemSpec) (filesystem.FileSystem, err
var (
size, start int64
)

rwBackingFile, err := d.Backend.Writable()
if err != nil {
return nil, err
}

switch {
case !d.Writable:
return nil, errIncorrectOpenMode
case spec.Partition == 0:
size = d.Size
start = 0
Expand All @@ -183,11 +176,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(rwBackingFile, size, start, d.LogicalBlocksize, spec.VolumeLabel)
case filesystem.TypeISO9660:
return iso9660.Create(d.File, size, start, d.LogicalBlocksize, spec.WorkDir)
return iso9660.Create(rwBackingFile, size, start, d.LogicalBlocksize, spec.WorkDir)
case filesystem.TypeExt4:
return ext4.Create(d.File, size, start, d.LogicalBlocksize, nil)
return ext4.Create(rwBackingFile, size, start, d.LogicalBlocksize, nil)
case filesystem.TypeSquashfs:
return nil, filesystem.ErrReadonlyFilesystem
default:
Expand Down Expand Up @@ -228,7 +221,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
}
Expand All @@ -238,17 +231,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
}
Expand All @@ -258,7 +251,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{}
Expand Down
Loading

0 comments on commit d44b7c9

Please sign in to comment.