blob: af7f98683c0a2a47ac7a75044415c6d2107a1db2 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package multipart implements an http.File that acts as one logical file
// backed by several physical files (the 'parts').
package multipart
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
var internalErr = fmt.Errorf("internal error")
// NewFile creates the multipart file out of the provided parts.
// The sizes of the parts are captured at the outset and not updated
// for the lifetime of the multipart file (any subsequent modifications
// in the parts will cause Read and Seek to work incorrectly).
func NewFile(name string, parts []*os.File) (http.File, error) {
fileParts := make([]filePart, len(parts))
for i, p := range parts {
stat, err := p.Stat()
if err != nil {
return nil, err
}
size := stat.Size()
// TODO(caprita): we can relax this restriction later.
if size == 0 {
return nil, fmt.Errorf("Part is empty")
}
fileParts[i] = filePart{file: p, size: size}
}
return &multipartFile{name: name, parts: fileParts}, nil
}
type filePart struct {
file *os.File
size int64
}
type multipartFile struct {
name string
parts []filePart
activePart int
partOffset int64
}
func (m *multipartFile) currPos() (res int64) {
for i := 0; i < m.activePart; i++ {
res += m.parts[i].size
}
res += m.partOffset
return
}
func (m *multipartFile) totalSize() (res int64) {
for _, p := range m.parts {
res += p.size
}
return
}
// Readdir is not implemented.
func (*multipartFile) Readdir(int) ([]os.FileInfo, error) {
return nil, fmt.Errorf("Not implemented")
}
type fileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
// Name returns the name of the multipart file.
func (f *fileInfo) Name() string {
return f.name
}
// Size returns the size of the multipart file (the sum of all parts).
func (f *fileInfo) Size() int64 {
return f.size
}
// Mode is currently hardcoded to 0700.
func (f *fileInfo) Mode() os.FileMode {
return f.mode
}
// ModTime is set to the current time.
func (f *fileInfo) ModTime() time.Time {
return f.modTime
}
// IsDir always returns false.
func (f *fileInfo) IsDir() bool {
return false
}
// Sys always returns nil.
func (f *fileInfo) Sys() interface{} {
return nil
}
// Stat describes the multipart file.
func (m *multipartFile) Stat() (os.FileInfo, error) {
return &fileInfo{
name: m.name,
size: m.totalSize(),
mode: 0700,
modTime: time.Now(),
}, nil
}
// Close closes all the parts.
func (m *multipartFile) Close() error {
var lastErr error
for _, p := range m.parts {
if err := p.file.Close(); err != nil {
lastErr = err
}
}
return lastErr
}
// Read reads from the parts in sequence.
func (m *multipartFile) Read(buf []byte) (int, error) {
if m.activePart >= len(m.parts) {
return 0, io.EOF
}
p := m.parts[m.activePart]
n, err := p.file.Read(buf)
m.partOffset += int64(n)
if m.partOffset > p.size {
// Likely, the file has changed.
return 0, internalErr
}
if m.partOffset == p.size {
m.activePart++
if m.activePart < len(m.parts) {
if _, err := m.parts[m.activePart].file.Seek(0, 0); err != nil {
return 0, err
}
m.partOffset = 0
}
}
return n, err
}
// Seek seeks into the part corresponding to the global offset.
func (m *multipartFile) Seek(offset int64, whence int) (int64, error) {
var target int64
switch whence {
case 0:
target = offset
case 1:
target = m.currPos() + offset
case 2:
target = m.totalSize() - offset
default:
return 0, fmt.Errorf("invalid whence: %d", whence)
}
if target < 0 || target > m.totalSize() {
return 0, fmt.Errorf("invalid offset")
}
var c int64
for i, p := range m.parts {
if pSize := p.size; c+pSize <= target {
c += pSize
continue
}
m.activePart = i
if _, err := p.file.Seek(target-c, 0); err != nil {
return 0, err
}
m.partOffset = target - c
return target, nil
}
// target <= m.totalSize() should ensure this is never reached.
return 0, internalErr // Should not be reached.
}