dragonfly/client/dfcache/dfcache.go

301 lines
7.7 KiB
Go

/*
* Copyright 2022 The Dragonfly Authors
*
* 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 dfcache
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"time"
commonv1 "d7y.io/api/pkg/apis/common/v1"
dfdaemonv1 "d7y.io/api/pkg/apis/dfdaemon/v1"
"d7y.io/dragonfly/v2/client/config"
"d7y.io/dragonfly/v2/internal/dferrors"
logger "d7y.io/dragonfly/v2/internal/dflog"
dfdaemonclient "d7y.io/dragonfly/v2/pkg/rpc/dfdaemon/client"
)
// Format that's used to cast the given cid to URI.
// "d7y" as scheme, and absolute path starts with "/"
const cidURIFormat = "d7y:/%s"
func newCid(cid string) string {
return fmt.Sprintf(cidURIFormat, url.QueryEscape(cid))
}
// Stat checks if the given cache entry exists in local storage and/or in P2P network, and returns
// os.ErrNotExist if cache is not found.
func Stat(cfg *config.DfcacheConfig, client dfdaemonclient.V1) error {
var (
ctx = context.Background()
cancel context.CancelFunc
statError error
)
if err := cfg.Validate(config.CmdStat); err != nil {
return fmt.Errorf("validate stat option failed: %w", err)
}
wLog := logger.With("Cid", cfg.Cid, "Tag", cfg.Tag)
wLog.Info("init success and start to stat")
if cfg.Timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, cfg.Timeout)
} else {
ctx, cancel = context.WithCancel(ctx)
}
go func() {
statError = statTask(ctx, client, cfg, wLog)
cancel()
}()
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("stat timeout(%s)", cfg.Timeout)
}
return statError
}
func statTask(ctx context.Context, client dfdaemonclient.V1, cfg *config.DfcacheConfig, wLog *logger.SugaredLoggerOnWith) error {
if client == nil {
return errors.New("stat has no daemon client")
}
start := time.Now()
statError := client.StatTask(ctx, newStatRequest(cfg))
if statError == nil {
wLog.Infof("task found in %.6f s", time.Since(start).Seconds())
return nil
}
// Task not found, return os.ErrNotExist
if dferrors.CheckError(statError, commonv1.Code_PeerTaskNotFound) {
return os.ErrNotExist
}
// Otherwise hit internal error
wLog.Errorf("daemon stat file error: %s", statError)
return statError
}
func newStatRequest(cfg *config.DfcacheConfig) *dfdaemonv1.StatTaskRequest {
return &dfdaemonv1.StatTaskRequest{
Url: newCid(cfg.Cid),
UrlMeta: &commonv1.UrlMeta{
Tag: cfg.Tag,
},
LocalOnly: cfg.LocalOnly,
}
}
// Import imports the given cache into P2P network.
func Import(cfg *config.DfcacheConfig, client dfdaemonclient.V1) error {
var (
ctx = context.Background()
cancel context.CancelFunc
importError error
)
if err := cfg.Validate(config.CmdImport); err != nil {
return fmt.Errorf("validate import option failed: %w", err)
}
wLog := logger.With("Cid", cfg.Cid, "Tag", cfg.Tag, "file", cfg.Path)
wLog.Info("init success and start to import")
if cfg.Timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, cfg.Timeout)
} else {
ctx, cancel = context.WithCancel(ctx)
}
go func() {
importError = importTask(ctx, client, cfg, wLog)
cancel()
}()
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("import timeout(%s)", cfg.Timeout)
}
return importError
}
func importTask(ctx context.Context, client dfdaemonclient.V1, cfg *config.DfcacheConfig, wLog *logger.SugaredLoggerOnWith) error {
if client == nil {
return errors.New("import has no daemon client")
}
start := time.Now()
importError := client.ImportTask(ctx, newImportRequest(cfg))
if importError != nil {
wLog.Errorf("daemon import file error: %s", importError)
return importError
}
wLog.Infof("task imported successfully in %.6f s", time.Since(start).Seconds())
return nil
}
func newImportRequest(cfg *config.DfcacheConfig) *dfdaemonv1.ImportTaskRequest {
return &dfdaemonv1.ImportTaskRequest{
Type: commonv1.TaskType_DfCache,
Url: newCid(cfg.Cid),
Path: cfg.Path,
UrlMeta: &commonv1.UrlMeta{
Tag: cfg.Tag,
},
}
}
// Export exports or downloads the given cache from P2P network, and return os.ErrNotExist if cache
// doesn't exist.
func Export(cfg *config.DfcacheConfig, client dfdaemonclient.V1) error {
var (
ctx = context.Background()
cancel context.CancelFunc
exportError error
)
if err := cfg.Validate(config.CmdExport); err != nil {
return fmt.Errorf("validate export option failed: %w", err)
}
wLog := logger.With("Cid", cfg.Cid, "Tag", cfg.Tag, "output", cfg.Output)
wLog.Info("init success and start to export")
if cfg.Timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, cfg.Timeout)
} else {
ctx, cancel = context.WithCancel(ctx)
}
go func() {
exportError = exportTask(ctx, client, cfg, wLog)
cancel()
}()
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("export timeout(%s)", cfg.Timeout)
}
return exportError
}
func exportTask(ctx context.Context, client dfdaemonclient.V1, cfg *config.DfcacheConfig, wLog *logger.SugaredLoggerOnWith) error {
if client == nil {
return errors.New("export has no daemon client")
}
start := time.Now()
exportError := client.ExportTask(ctx, newExportRequest(cfg))
if exportError == nil {
wLog.Infof("task exported successfully in %.6f s", time.Since(start).Seconds())
return nil
}
// Task not found, return os.ErrNotExist
if dferrors.CheckError(exportError, commonv1.Code_PeerTaskNotFound) {
return os.ErrNotExist
}
// Otherwise hit internal error
wLog.Errorf("daemon export file error: %s", exportError)
return exportError
}
func newExportRequest(cfg *config.DfcacheConfig) *dfdaemonv1.ExportTaskRequest {
return &dfdaemonv1.ExportTaskRequest{
Url: newCid(cfg.Cid),
Output: cfg.Output,
Timeout: uint64(cfg.Timeout),
Limit: float64(cfg.RateLimit),
UrlMeta: &commonv1.UrlMeta{
Tag: cfg.Tag,
},
Uid: int64(os.Getuid()),
Gid: int64(os.Getgid()),
LocalOnly: cfg.LocalOnly,
}
}
func Delete(cfg *config.DfcacheConfig, client dfdaemonclient.V1) error {
var (
ctx = context.Background()
cancel context.CancelFunc
deleteError error
)
if err := cfg.Validate(config.CmdDelete); err != nil {
return fmt.Errorf("validate delete option failed: %w", err)
}
wLog := logger.With("Cid", cfg.Cid, "Tag", cfg.Tag)
wLog.Info("init success and start to delete")
if cfg.Timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, cfg.Timeout)
} else {
ctx, cancel = context.WithCancel(ctx)
}
go func() {
deleteError = deleteTask(ctx, client, cfg, wLog)
cancel()
}()
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("delete timeout(%s)", cfg.Timeout)
}
return deleteError
}
func deleteTask(ctx context.Context, client dfdaemonclient.V1, cfg *config.DfcacheConfig, wLog *logger.SugaredLoggerOnWith) error {
if client == nil {
return errors.New("delete has no daemon client")
}
start := time.Now()
deleteError := client.DeleteTask(ctx, newDeleteRequest(cfg))
if deleteError != nil {
wLog.Errorf("daemon delete file error: %s", deleteError)
return deleteError
}
wLog.Infof("task deleted successfully in %.6f s", time.Since(start).Seconds())
return nil
}
func newDeleteRequest(cfg *config.DfcacheConfig) *dfdaemonv1.DeleteTaskRequest {
return &dfdaemonv1.DeleteTaskRequest{
Url: newCid(cfg.Cid),
UrlMeta: &commonv1.UrlMeta{
Tag: cfg.Tag,
},
}
}