A pure-userspace SCSI tape (SSC) driver over iSCSI, built on uiscsi.
Status: Full record-oriented tape I/O with 2-deep command pipelining and bounded-memory streaming. Variable and fixed block modes. Configurable TUR retry for media load detection. Write atomicity guaranteed at the SCSI level.
uiscsi-tape wraps a uiscsi.Session to speak SSC (SCSI Stream Commands) to tape drives over iSCSI. It handles drive probing, block limit negotiation, record-oriented I/O, and tape-specific error conditions (filemark, end-of-medium, blank check, incorrect length).
Read operations use sess.Raw().StreamExecute internally for bounded-memory streaming (~64KB) suitable for tape's large block sizes (256KB-4MB) at sustained throughput (400+ MB/s). Drive configuration (block size, compression) uses sess.SCSI().ModeSelect6.
import (
"github.com/uiscsi/uiscsi"
tape "github.com/uiscsi/uiscsi-tape"
)
// Connect to an iSCSI target.
// For tape performance, increase MaxRecvDataSegmentLength from the
// default 8KB. 256KB is a good choice for LTO drives.
sess, err := uiscsi.Dial(ctx, "192.168.1.100:3260",
uiscsi.WithTarget("iqn.2026-03.com.example:tape"),
uiscsi.WithMaxRecvDataSegmentLength(262144),
)
if err != nil { log.Fatal(err) }
defer sess.Close()
// Probe the tape drive.
drive, err := tape.Open(ctx, sess, 0)
if err != nil { log.Fatal(err) }
fmt.Printf("Drive: %s %s\n", drive.Info().VendorID, drive.Info().ProductID)
fmt.Printf("Block limits: min=%d max=%d\n", drive.Limits().MinBlock, drive.Limits().MaxBlock)
// Write a record.
if err := drive.Write(ctx, []byte("hello tape")); err != nil {
log.Fatal(err)
}
// Write a filemark (record separator).
if err := drive.WriteFilemarks(ctx, 1); err != nil {
log.Fatal(err)
}
// Rewind and read back.
if err := drive.Rewind(ctx); err != nil {
log.Fatal(err)
}
buf := make([]byte, 65536)
n, err := drive.Read(ctx, buf)
if err != nil { log.Fatal(err) }
fmt.Printf("Read %d bytes: %s\n", n, buf[:n])- Drive probing -- TEST UNIT READY + INQUIRY (device type 0x01 check) + READ BLOCK LIMITS
- Configurable TUR retry --
WithTURRetryIntervalandWithTURRetryCountcontrol howOpen()polls TEST UNIT READY while the drive loads media; UNIT ATTENTION and NOT READY are normal during this window - Record I/O --
ReadandWritefor record-oriented tape access - Write atomicity -- each
Write()call maps to a single SCSI WRITE(6) command; the drive either writes the complete record or returns an error (see Write Atomicity below) - Tape control --
WriteFilemarksfor logical record separation,Rewindfor repositioning,Positionfor block position query,Closefor cleanup - Variable-block mode -- default, each record can be a different size
- Fixed-block mode -- via
WithBlockSize(n), configures drive via MODE SELECT and reads/writes in fixed-size blocks - SILI support -- via
WithSILI(true), suppresses ILI on short reads - Hardware compression --
Compression/SetCompressionfor drive-level compression (LTO) - Read-ahead pipeline --
WithReadAhead(1)enables 2-deep command pipelining, hiding network RTT and providing continuous tape motion on sequential reads - Bounded-memory streaming -- Read uses
sess.Raw().StreamExecute(~64KB peak memory regardless of block size) - Tape-specific errors --
TapeErrorwith Filemark, EOM, ILI, BlankCheck condition flags - Sentinel errors --
ErrFilemark,ErrEOM,ErrBlankCheck,ErrILI,ErrNotTapeforerrors.Ismatching - Sense parsing -- uses
uiscsi.ParseSenseDatafor SPC-4 parsing, adds tape-specific wrapping - slog diagnostics --
WithLoggerinjects aslog.Loggerfor drive operations and error events
Each Write() call issues a single SCSI WRITE(6) command to the drive. The drive either writes the complete record — exactly the bytes passed to Write() — or returns an error. There are no partial records written to tape.
This guarantee matters for applications that need to reason about tape content after an error. If Write() returns an error, the tape position is either at the start of the failed record (if the drive rejected the command) or the application should treat the tape state as undefined and rewind before continuing. Applications writing structured data should write filemarks at logical boundaries so that partial content can be identified and skipped on re-read.
| Function/Type | Description |
|---|---|
Open |
Probe a LUN and return a Drive if it is a tape device |
Drive.Read |
Read one record from current position into buffer |
Drive.Write |
Write one record at current position (atomic at SCSI level) |
Drive.WriteFilemarks |
Write N filemarks at current position |
Drive.Rewind |
Reposition to beginning of tape |
Drive.Position |
Query current logical block number (READ POSITION) |
Drive.BlockSize |
Query drive's current block size (MODE SENSE) |
Drive.SetBlockSize |
Set drive's block size (MODE SELECT) |
Drive.Compression |
Query drive compression settings |
Drive.SetCompression |
Enable/disable hardware compression |
Drive.Close |
Restore variable-block mode if fixed was configured |
Drive.Info |
Drive identification from INQUIRY |
Drive.Limits |
Block size limits from READ BLOCK LIMITS |
WithBlockSize |
Configure fixed-block mode (0 = variable, default) |
WithReadAhead |
Pre-fetch depth for sequential read throughput (0 = disabled) |
WithSILI |
Suppress Incorrect Length Indicator on short reads |
WithLogger |
Inject slog.Logger for diagnostics |
WithTURRetryInterval |
Interval between TEST UNIT READY polls during Open() |
WithTURRetryCount |
Maximum number of TUR retries during Open() |
TapeError |
Error type with tape condition flags |
ErrFilemark |
Sentinel: filemark encountered during read |
ErrEOM |
Sentinel: end-of-medium reached |
ErrBlankCheck |
Sentinel: blank/unwritten area encountered |
ErrILI |
Sentinel: incorrect length (block size mismatch) |
Tape conditions are returned as *TapeError supporting errors.Is:
n, err := drive.Read(ctx, buf)
if errors.Is(err, tape.ErrFilemark) {
// Filemark -- logical end of file on tape. Normal condition.
}
if errors.Is(err, tape.ErrEOM) {
// End of medium -- stop writing soon.
}
if errors.Is(err, tape.ErrBlankCheck) {
// No more data on tape.
}
if errors.Is(err, tape.ErrILI) {
// Record was shorter than buffer (n has actual bytes read).
// Use WithSILI(true) to suppress this and get (n, nil) instead.
}- Go 1.25 or later
- github.com/uiscsi/uiscsi
- Tests require goleak for goroutine leak detection (test-only; not imported by library code)