Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Allows to open disk from already existing fs.File descriptor #265

Merged
merged 1 commit into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

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:

Expand Down
127 changes: 127 additions & 0 deletions backend/file/file.go
Original file line number Diff line number Diff line change
@@ -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
}
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
}
aol-nnov marked this conversation as resolved.
Show resolved Hide resolved

type Storage interface {
File
// OS-specific file for ioctl calls via fd
Sys() (*os.File, error)
// file for read-write operations
Writable() (WritableFile, error)
}
64 changes: 26 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 {
deitch marked this conversation as resolved.
Show resolved Hide resolved
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,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
Expand All @@ -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:
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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{}
Expand Down
Loading
Loading