Files
cloudpods/cmd/fetcherfs/fs.go
2022-12-27 01:21:26 +08:00

282 lines
6.3 KiB
Go

// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"strconv"
"strings"
"sync"
"syscall"
"bazil.org/fuse/fs"
"github.com/pierrec/lz4/v4"
"github.com/pkg/errors"
"yunion.io/x/log"
"yunion.io/x/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/util/bitmap"
)
// FetcherFs implements the feteher file system.
type FetcherFs struct {
blocksize int64
url string
size int64
receivedSize int64
blockCount int64
fetchedCount int64
blockBitMap *bitmap.BitMap
readLock sync.Mutex
localFile *os.File
}
func (FetcherFs) Root() (fs.Node, error) {
return Dir{}, nil
}
var fetcherFs *FetcherFs
func initFetcherFs() (*FetcherFs, error) {
fetcherFs = &FetcherFs{
blocksize: int64(opt.Blocksize) * 1024 * 1024,
url: opt.Url,
}
if err := fetcherFs.fetchMetaInfo(); err != nil {
return nil, errors.Wrapf(err, "fetch meta info")
}
segs := strings.Split(opt.Url, "/")
if len(segs[len(segs)-1]) == 0 {
return nil, errors.Errorf("bad url: %s", opt.Url)
}
fd, err := ioutil.TempFile(opt.Tmpdir, fmt.Sprintf("%s.*", segs[len(segs)-1]))
if err != nil {
return nil, errors.Wrap(err, "create tempfile")
}
err = syscall.Fallocate(int(fd.Fd()), 0, 0, fetcherFs.size)
if err != nil {
os.Remove(path.Join(opt.Tmpdir, fd.Name()))
return nil, errors.Wrap(err, "fallocate temp file")
}
fetcherFs.localFile = fd
return fetcherFs, nil
}
func destoryInitFetcherFs() error {
if fetcherFs == nil {
return nil
}
log.Errorf("Remove path %s", fetcherFs.localFile.Name())
err := os.Remove(fetcherFs.localFile.Name())
if err != nil {
return err
}
return nil
}
func (fs *FetcherFs) fetchMetaInfo() error {
header := NewRequestHeader()
res, _, err := httputils.JSONRequest(
httputils.GetDefaultClient(),
context.Background(),
http.MethodHead,
opt.Url,
header,
nil,
false,
)
if err != nil {
return err
}
if lengthStr := res.Get("Content-Length"); lengthStr != "" {
fs.size, err = strconv.ParseInt(lengthStr, 10, 0)
if err != nil {
return errors.Wrap(err, "parse Content-Length")
}
fs.blockCount = fs.size / fs.blocksize
if fs.size%fs.blocksize > 0 {
fs.blockCount += 1
}
fs.blockBitMap = bitmap.NewBitMap(fs.blockCount)
} else {
return errors.Errorf("not found Content-Length")
}
return nil
}
func (fs *FetcherFs) GetSize() int64 {
return fs.size
}
func (fs *FetcherFs) GetBlockCount() int64 {
return fs.blockCount
}
func (fs *FetcherFs) GetFetchedCount() int64 {
return fs.fetchedCount
}
func (fs *FetcherFs) GetReceivedSize() int64 {
return fs.receivedSize
}
func (fs *FetcherFs) GetMeta() string {
return fmt.Sprintf(
"pid=%d\nblock_count=%d\nfetch_count=%d\nrecvsize=%d\ntoken=%s\n",
os.Getpid(), fs.GetBlockCount(), fs.GetFetchedCount(),
fs.GetReceivedSize(), opt.Token,
)
}
func (fs *FetcherFs) doRead(size int, offset int64) ([]byte, error) {
fs.readLock.Lock()
defer fs.readLock.Unlock()
var (
start, end = fs.offsetToBlockIndexRange(size, offset)
err error
)
for idx := start; idx <= end; idx++ {
err = fs.fetchData(idx)
if err != nil {
return nil, errors.Wrap(err, "fetch data")
}
}
_, err = fs.localFile.Seek(offset, io.SeekStart)
if err != nil {
return nil, errors.Wrap(err, "seek local file")
}
var buf = make([]byte, size)
n, err := fs.localFile.Read(buf)
if err != nil {
return nil, errors.Wrap(err, "do read")
}
return buf[:n], nil
}
func (fs *FetcherFs) fetchData(idx int64) (err error) {
for i := 0; i < 3; i++ {
if err = fs.doFetchData(idx); err != nil {
log.Errorf("fetch data %d failed: %s", idx, err)
} else {
break
}
}
return
}
func (fs *FetcherFs) blockReady(idx int64) bool {
return fs.blockBitMap.Has(idx)
}
func (fs *FetcherFs) setBlockReady(idx int64) {
fs.blockBitMap.Set(idx)
}
func (fs *FetcherFs) doFetchData(idx int64) error {
if fs.blockReady(idx) {
return nil
}
log.Infof("start do fetch idx %d ", idx)
var start = idx * fs.blocksize
var end = start + fs.blocksize - 1
if end >= fs.size {
end = fs.size - 1
}
var header = NewRequestHeader()
header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
log.Errorf("Range %s", header.Get("Range"))
var (
res, err = httputils.Request(
httputils.GetDefaultClient(),
context.Background(),
http.MethodGet,
fs.url, header,
nil, false,
)
)
if err != nil {
return errors.Wrap(err, "http fetch data")
}
defer res.Body.Close()
if res.StatusCode >= 300 {
body, _ := ioutil.ReadAll(res.Body)
return errors.Errorf("failed fetch data: %d %s", res.StatusCode, body)
}
var lz4Reader = lz4.NewReader(res.Body)
if err := lz4Reader.Apply(lz4.ConcurrencyOption(-1)); err != nil {
return errors.Errorf("Apply lz4 option: %v", err)
}
buf, err := ioutil.ReadAll(lz4Reader)
if len(buf) != int(end-start+1) {
return errors.Wrap(err, "written local file")
}
written, err := fs.localFile.WriteAt(buf, start)
if err != nil {
return errors.Wrap(err, "write local file")
}
if err := fs.localFile.Sync(); err != nil {
log.Errorf("sync failed %s", err)
}
// on write success
fs.setBlockReady(idx)
fs.receivedSize += int64(written)
fs.fetchedCount += 1
return nil
}
// fetcherfs byte range to block range
func (fs *FetcherFs) offsetToBlockIndexRange(size int, offset int64) (int64, int64) {
return offset / int64(fs.blocksize), (offset + int64(size) - 1) / fs.blocksize
}
func (fs *FetcherFs) destory() error {
var header = NewRequestHeader()
_, err := httputils.Request(
httputils.GetDefaultClient(),
context.Background(),
http.MethodPost,
fs.url, header,
nil, false,
)
return err
}
func NewRequestHeader() http.Header {
header := http.Header{}
header.Set("X-Auth-Token", opt.Token)
if len(opt.EncryptKey) > 0 {
header.Set("X-Encrypt-Key", opt.EncryptKey)
}
if len(opt.EncryptAlg) > 0 {
header.Set("X-Encrypt-Alg", opt.EncryptAlg)
}
return header
}