diff --git a/.github/workflows/build-project.yml b/.github/workflows/build-project.yml index 046f2ed..7f74703 100644 --- a/.github/workflows/build-project.yml +++ b/.github/workflows/build-project.yml @@ -2,7 +2,7 @@ name: Build Project on: pull_request: branches: - - "main" + - "*" jobs: build-and-test: diff --git a/.vscode/settings.json b/.vscode/settings.json index 5ea2401..4abb2e7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "errgroup", "oget", + "oomol", "Wrapf" ] } \ No newline at end of file diff --git a/README.md b/README.md index 9668c3a..547b751 100644 --- a/README.md +++ b/README.md @@ -1 +1,165 @@ -# oget \ No newline at end of file +# oget + +oget is a Golang download library. It supports parallel downloads, resuming after failures, SHA512 verification, and download progress monitoring. + +## Installation + +```shell +$ go install github.com/oomol-lab/oget +``` + +## Quick Start + +```go +import "github.com/oomol-lab/oget" + +task, err := oget.CreateGettingTask(&oget.RemoteFile{ + URL: "https://github.com/oomol-lab/oget/raw/main/tests/target.bin", +}) +if err != nil { + panic(err) +} +_, err = task.Get(&oget.GettingConfig{ + FilePath: "/path/to/save/file.bin", +}) +if err != nil { + panic(err) +} +``` + +## Advanced Features + +### Segmented Download + +It splits large files into multiple smaller parts for parallel downloading, then merges them into one large file. Additionally, you can specify the temporary directory for storing the split files by adding the `PartsPath` field. + +```go +import "github.com/oomol-lab/oget" + +task, err := oget.CreateGettingTask(&oget.RemoteFile{ + URL: "https://github.com/oomol-lab/oget/raw/main/tests/target.bin", +}) +if err != nil { + panic(err) +} +_, err = task.Get(&oget.GettingConfig{ + // Path to store the final file + FilePath: "/path/to/save/file.bin", + // Number of file parts for splitting and parallel downloading + Parts: 4, + // If not specified, defaults to the same directory as `FilePath` + PartsPath: "/path/to/save/temp/files", +}) +if err != nil { + panic(err) +} +``` + +### Download Progress Monitoring + +`ListenProgress` is not thread-safe and may be called in multiple threads. You need to manually lock it to ensure thread safety. + +```go +import "github.com/oomol-lab/oget" + +task, err := oget.CreateGettingTask(&oget.RemoteFile{ + URL: "https://github.com/oomol-lab/oget/raw/main/tests/target.bin", +}) +if err != nil { + panic(err) +} +var mux sync.Mutex +_, err = task.Get(&oget.GettingConfig{ + FilePath: "/path/to/save/file.bin", + ListenProgress: func(event oget.ProgressEvent) { + // Callback may be invoked in multiple threads + // Use a lock to ensure thread safety + mux.Lock() + defer mux.Unlock() + switch event.phase { + case oget.ProgressPhaseDownloading: + // Progress of downloading from the network + case oget.ProgressPhaseCoping: + // Download complete, merging multiple file parts into one file + case oget.ProgressPhaseDone: + // All tasks are completed + } + // Number of bytes completed in this step + progress := event.Progress + // Total number of bytes in this step + total := event.Total + }, +}) +if err != nil { + panic(err) +} +``` + +### SHA512 Verification + +After downloading, the library performs a SHA512 checksum on the entire file. If the checksum fails, an `oget.SHA512Error` is thrown. + +```go +import "github.com/oomol-lab/oget" + +task, err := oget.CreateGettingTask(&oget.RemoteFile{ + URL: "https://github.com/oomol-lab/oget/raw/main/tests/target.bin", +}) +if err != nil { + panic(err) +} +_, err = task.Get(&oget.GettingConfig{ + FilePath: "/path/to/save/file.bin", + SHA512: "d286fbb1fab9014fdbc543d09f54cb93da6e0f2c809e62ee0c81d69e4bf58eec44571fae192a8da9bc772ce1340a0d51ad638cdba6118909b555a12b005f2930", +}) +if err != nil { + if sha512Error, ok := err.(oget.SHA512Error); ok { + // Failed due to SHA512 verification failure + } + panic(err) +} +``` + +### Resuming Downloads + +During a download, oget creates a temporary file with the extension `*.downloading` (regardless of whether it's split into parts). If a download fails due to network issues and the temporary file is not deleted, resuming the download will retain the progress from the previous attempt. To implement resuming downloads, ignore download failures caused by network issues and retry the download. + +First, create a `task` object using `oget.CreateGettingTask`. This step only fetches file metadata from the server without starting the download. If this step fails, it should be considered a complete failure of the download task. + +```go +import "github.com/oomol-lab/oget" + +task, err := oget.CreateGettingTask(&oget.RemoteFile{ + URL: "https://github.com/oomol-lab/oget/raw/main/tests/target.bin", +}) +if err != nil { + panic(err) +} +``` + +Then, call `task.Get()` to initiate the download. Check if the error is of type `oget.SHA512Error`. If not, it is likely due to network issues and should be retried. + +Note that the first return value of `task.Get()` is a function `clean` that deletes the temporary download files. Call it to free up disk space if you don't want to keep these files for the next download attempt after a download failure. + +```go +success := false + +for i := 0; i < 10; i++ { + clean, err = task.Get(&oget.GettingConfig{ + FilePath: "/path/to/save/file.bin", + Parts: 4, + }) + if err != nil { + if sha512Error, ok := err.(oget.SHA512Error); ok { + clean() + panic(sha512Error) + } + fmt.Printf("download failed with error and retry %s", err) + } else { + success = true + } +} +if !success { + panic("download fail") +} +``` \ No newline at end of file diff --git a/config.go b/config.go index ad3caa2..3217100 100644 --- a/config.go +++ b/config.go @@ -8,20 +8,42 @@ import ( ) type RemoteFile struct { - Context context.Context - Timeout time.Duration - URL string - Useragent string - Referer string + // the context for the http request. + // if the value is nil, the context will be context.Background(). + Context context.Context + // the maximum amount of time a dial will wait for a connect or copy to complete. + // the default is 10 seconds. + Timeout time.Duration + // the URL of the file to download. + URL string + // the User-Agent header field value. + // if the value is empty, the User-Agent header will not be set. + Useragent string + // the Referer header field value. + // if the value is empty, the Referer header will not be set. + Referer string + // the maximum number of idle (keep-alive) connections to keep per-host. + // the default is 16. MaxIdleConnsPerHost int } type GettingConfig struct { - FilePath string - SHA512 string - PartsPath string - PartName string - Parts int + // the path to save the downloaded file. + FilePath string + // the SHA512 code of the file. + // if the code is empty, the file will not be checked. + SHA512 string + // PartsPath is the path to save the temp files of downloaded parts. + // if the value is empty, the temp files will be saved in the same directory as the FilePath. + PartsPath string + // the name of the part file. + // if the value is empty, the name will be the same as the file name. + PartName string + // the number of parts to download the file. + // if the value is less than or equal to 0, the file will be downloaded in one part. + Parts int + // the progress listener. + // if the value is nil, the progress will not be listened. ListenProgress ProgressListener } diff --git a/oget.go b/oget.go index f262c8f..980ff44 100644 --- a/oget.go +++ b/oget.go @@ -26,6 +26,7 @@ type GettingTask struct { timeout time.Duration } +// creates a new GettingTask. will access the URL to get the file information. func CreateGettingTask(config *RemoteFile) (*GettingTask, error) { c := config.standardize() client := newGettingClient(c.MaxIdleConnsPerHost) @@ -74,10 +75,12 @@ func CreateGettingTask(config *RemoteFile) (*GettingTask, error) { return task, nil } +// returns the content length of the file. func (t *GettingTask) ContentLength() int64 { return t.contentLength } +// downloads the file. func (t *GettingTask) Get(config *GettingConfig) (func() error, error) { var prog *progress c := config.standardize() diff --git a/progress.go b/progress.go index afac51a..1fdff58 100644 --- a/progress.go +++ b/progress.go @@ -8,16 +8,25 @@ import ( type ProgressPhase int const ( + // ProgressPhaseDownloading is the phase of downloading. ProgressPhaseDownloading ProgressPhase = iota + // ProgressPhaseCoping is the phase of merging from parts of temp files. ProgressPhaseCoping + // ProgressPhaseDone is the phase of downloading done. ProgressPhaseDone ) +// ProgressListener is the listener of the progress. type ProgressListener func(event ProgressEvent) + +// ProgressEvent is the event of the progress. type ProgressEvent struct { - Phase ProgressPhase + // the phase of the progress. + Phase ProgressPhase + // the progress of the downloading (bytes). Progress int64 - Total int64 + // the total length of the downloading (bytes). + Total int64 } type progress struct { diff --git a/sha512.go b/sha512.go index 4e98aca..4a20926 100644 --- a/sha512.go +++ b/sha512.go @@ -7,6 +7,7 @@ import ( "os" ) +// returns the SHA512 code of the file. func SHA512(path string) (string, error) { return sha512OfFiles(&[]string{path}) } @@ -27,6 +28,7 @@ func sha512OfFiles(pathList *[]string) (string, error) { return hashStr, nil } +// SHA512Error is the error type of SHA512. type SHA512Error struct { message string }