mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Add a cache for gallery thumbnails (#496)
This commit is contained in:
1
go.mod
1
go.mod
@@ -6,7 +6,6 @@ require (
|
|||||||
github.com/antchfx/htmlquery v1.2.3
|
github.com/antchfx/htmlquery v1.2.3
|
||||||
github.com/bmatcuk/doublestar v1.1.5
|
github.com/bmatcuk/doublestar v1.1.5
|
||||||
github.com/disintegration/imaging v1.6.0
|
github.com/disintegration/imaging v1.6.0
|
||||||
github.com/dustin/go-humanize v1.0.0
|
|
||||||
github.com/go-chi/chi v4.0.2+incompatible
|
github.com/go-chi/chi v4.0.2+incompatible
|
||||||
github.com/gobuffalo/packr/v2 v2.0.2
|
github.com/gobuffalo/packr/v2 v2.0.2
|
||||||
github.com/golang-migrate/migrate/v4 v4.3.1
|
github.com/golang-migrate/migrate/v4 v4.3.1
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -325,10 +325,8 @@ github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
|
|||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
|
|
||||||
github.com/golang-migrate/migrate/v4 v4.3.1 h1:3eR1NY+pplX+m6yJ1fQf5dFWX3fBgUtZfDiaS/kJVu4=
|
github.com/golang-migrate/migrate/v4 v4.3.1 h1:3eR1NY+pplX+m6yJ1fQf5dFWX3fBgUtZfDiaS/kJVu4=
|
||||||
github.com/golang-migrate/migrate/v4 v4.3.1/go.mod h1:mJ89KBgbXmM3P49BqOxRL3riNF/ATlg5kMhm17GA0dE=
|
github.com/golang-migrate/migrate/v4 v4.3.1/go.mod h1:mJ89KBgbXmM3P49BqOxRL3riNF/ATlg5kMhm17GA0dE=
|
||||||
github.com/golang-migrate/migrate/v4 v4.10.0 h1:76R6UL3BGnDTpYeittMtfpaNvGBH5zMZatO/fCzIjWo=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||||||
stashes
|
stashes
|
||||||
databasePath
|
databasePath
|
||||||
generatedPath
|
generatedPath
|
||||||
|
cachePath
|
||||||
maxTranscodeSize
|
maxTranscodeSize
|
||||||
maxStreamingTranscodeSize
|
maxStreamingTranscodeSize
|
||||||
forceMkv
|
forceMkv
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ input ConfigGeneralInput {
|
|||||||
databasePath: String
|
databasePath: String
|
||||||
"""Path to generated files"""
|
"""Path to generated files"""
|
||||||
generatedPath: String
|
generatedPath: String
|
||||||
|
"""Path to cache"""
|
||||||
|
cachePath: String
|
||||||
"""Max generated transcode size"""
|
"""Max generated transcode size"""
|
||||||
maxTranscodeSize: StreamingResolutionEnum
|
maxTranscodeSize: StreamingResolutionEnum
|
||||||
"""Max streaming transcode size"""
|
"""Max streaming transcode size"""
|
||||||
@@ -49,6 +51,8 @@ type ConfigGeneralResult {
|
|||||||
databasePath: String!
|
databasePath: String!
|
||||||
"""Path to generated files"""
|
"""Path to generated files"""
|
||||||
generatedPath: String!
|
generatedPath: String!
|
||||||
|
"""Path to cache"""
|
||||||
|
cachePath: String!
|
||||||
"""Max generated transcode size"""
|
"""Max generated transcode size"""
|
||||||
maxTranscodeSize: StreamingResolutionEnum
|
maxTranscodeSize: StreamingResolutionEnum
|
||||||
"""Max streaming transcode size"""
|
"""Max streaming transcode size"""
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ input GenerateMetadataInput {
|
|||||||
previews: Boolean!
|
previews: Boolean!
|
||||||
markers: Boolean!
|
markers: Boolean!
|
||||||
transcodes: Boolean!
|
transcodes: Boolean!
|
||||||
|
"""gallery thumbnails for cache usage"""
|
||||||
|
thumbnails: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input ScanMetadataInput {
|
input ScanMetadataInput {
|
||||||
|
|||||||
72
pkg/api/cache_thumbs.go
Normal file
72
pkg/api/cache_thumbs.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/manager/paths"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type thumbBuffer struct {
|
||||||
|
path string
|
||||||
|
dir string
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCacheThumb(dir string, path string, data []byte) *thumbBuffer {
|
||||||
|
t := thumbBuffer{dir: dir, path: path, data: data}
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeChan chan *thumbBuffer
|
||||||
|
var touchChan chan *string
|
||||||
|
|
||||||
|
func startThumbCache() { // TODO add extra wait, close chan code if/when stash gets a stop mode
|
||||||
|
|
||||||
|
writeChan = make(chan *thumbBuffer, 20)
|
||||||
|
go thumbnailCacheWriter()
|
||||||
|
}
|
||||||
|
|
||||||
|
//serialize file writes to avoid race conditions
|
||||||
|
func thumbnailCacheWriter() {
|
||||||
|
|
||||||
|
for thumb := range writeChan {
|
||||||
|
exists, _ := utils.FileExists(thumb.path)
|
||||||
|
if !exists {
|
||||||
|
err := utils.WriteFile(thumb.path, thumb.data)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Write error for thumbnail %s: %s ", thumb.path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// get thumbnail from cache, otherwise create it and store to cache
|
||||||
|
func cacheGthumb(gallery *models.Gallery, index int, width int) []byte {
|
||||||
|
thumbPath := paths.GetGthumbPath(gallery.Checksum, index, width)
|
||||||
|
exists, _ := utils.FileExists(thumbPath)
|
||||||
|
if exists { // if thumbnail exists in cache return that
|
||||||
|
content, err := ioutil.ReadFile(thumbPath)
|
||||||
|
if err == nil {
|
||||||
|
return content
|
||||||
|
} else {
|
||||||
|
logger.Errorf("Read Error for file %s : %s", thumbPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
data := gallery.GetThumbnail(index, width)
|
||||||
|
thumbDir := paths.GetGthumbDir(gallery.Checksum)
|
||||||
|
t := newCacheThumb(thumbDir, thumbPath, data)
|
||||||
|
writeChan <- t // write the file to cache
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// create all thumbs for a given gallery
|
||||||
|
func CreateGthumbs(gallery *models.Gallery) {
|
||||||
|
count := gallery.ImageCount()
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
cacheGthumb(gallery, i, models.DefaultGthumbWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,13 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
|||||||
config.Set(config.Generated, input.GeneratedPath)
|
config.Set(config.Generated, input.GeneratedPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.CachePath != nil {
|
||||||
|
if err := utils.EnsureDir(*input.CachePath); err != nil {
|
||||||
|
return makeConfigGeneralResult(), err
|
||||||
|
}
|
||||||
|
config.Set(config.Cache, input.CachePath)
|
||||||
|
}
|
||||||
|
|
||||||
if input.MaxTranscodeSize != nil {
|
if input.MaxTranscodeSize != nil {
|
||||||
config.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
config.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
|
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
|
||||||
manager.GetInstance().Generate(input.Sprites, input.Previews, input.Markers, input.Transcodes)
|
manager.GetInstance().Generate(input.Sprites, input.Previews, input.Markers, input.Transcodes, input.Thumbnails)
|
||||||
return "todo", nil
|
return "todo", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
|||||||
Stashes: config.GetStashPaths(),
|
Stashes: config.GetStashPaths(),
|
||||||
DatabasePath: config.GetDatabasePath(),
|
DatabasePath: config.GetDatabasePath(),
|
||||||
GeneratedPath: config.GetGeneratedPath(),
|
GeneratedPath: config.GetGeneratedPath(),
|
||||||
|
CachePath: config.GetCachePath(),
|
||||||
MaxTranscodeSize: &maxTranscodeSize,
|
MaxTranscodeSize: &maxTranscodeSize,
|
||||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||||
ForceMkv: config.GetForceMKV(),
|
ForceMkv: config.GetForceMKV(),
|
||||||
|
|||||||
@@ -23,11 +23,15 @@ func (rs galleryRoutes) Routes() chi.Router {
|
|||||||
|
|
||||||
func (rs galleryRoutes) File(w http.ResponseWriter, r *http.Request) {
|
func (rs galleryRoutes) File(w http.ResponseWriter, r *http.Request) {
|
||||||
gallery := r.Context().Value(galleryKey).(*models.Gallery)
|
gallery := r.Context().Value(galleryKey).(*models.Gallery)
|
||||||
|
if gallery == nil {
|
||||||
|
http.Error(w, http.StatusText(404), 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
fileIndex, _ := strconv.Atoi(chi.URLParam(r, "fileIndex"))
|
fileIndex, _ := strconv.Atoi(chi.URLParam(r, "fileIndex"))
|
||||||
thumb := r.URL.Query().Get("thumb")
|
thumb := r.URL.Query().Get("thumb")
|
||||||
w.Header().Add("Cache-Control", "max-age=604800000") // 1 Week
|
w.Header().Add("Cache-Control", "max-age=604800000") // 1 Week
|
||||||
if thumb == "true" {
|
if thumb == "true" {
|
||||||
_, _ = w.Write(gallery.GetThumbnail(fileIndex, 200))
|
_, _ = w.Write(cacheGthumb(gallery, fileIndex, models.DefaultGthumbWidth))
|
||||||
} else if thumb == "" {
|
} else if thumb == "" {
|
||||||
_, _ = w.Write(gallery.GetImage(fileIndex))
|
_, _ = w.Write(gallery.GetImage(fileIndex))
|
||||||
} else {
|
} else {
|
||||||
@@ -36,7 +40,7 @@ func (rs galleryRoutes) File(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, http.StatusText(400), 400)
|
http.Error(w, http.StatusText(400), 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = w.Write(gallery.GetThumbnail(fileIndex, int(width)))
|
_, _ = w.Write(cacheGthumb(gallery, fileIndex, int(width)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ func Start() {
|
|||||||
http.Redirect(w, r, "/", 301)
|
http.Redirect(w, r, "/", 301)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
startThumbCache()
|
||||||
// Serve the web app
|
// Serve the web app
|
||||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
ext := path.Ext(r.URL.Path)
|
ext := path.Ext(r.URL.Path)
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ func (s *singleton) Export() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcodes bool) {
|
func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcodes bool, thumbnails bool) {
|
||||||
if s.Status.Status != Idle {
|
if s.Status.Status != Idle {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -179,6 +179,7 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
|||||||
s.Status.indefiniteProgress()
|
s.Status.indefiniteProgress()
|
||||||
|
|
||||||
qb := models.NewSceneQueryBuilder()
|
qb := models.NewSceneQueryBuilder()
|
||||||
|
qg := models.NewGalleryQueryBuilder()
|
||||||
//this.job.total = await ObjectionUtils.getCount(Scene);
|
//this.job.total = await ObjectionUtils.getCount(Scene);
|
||||||
instance.Paths.Generated.EnsureTmpDir()
|
instance.Paths.Generated.EnsureTmpDir()
|
||||||
|
|
||||||
@@ -186,6 +187,8 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
|||||||
defer s.returnToIdleState()
|
defer s.returnToIdleState()
|
||||||
|
|
||||||
scenes, err := qb.All()
|
scenes, err := qb.All()
|
||||||
|
var galleries []*models.Gallery
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("failed to get scenes for generate")
|
logger.Errorf("failed to get scenes for generate")
|
||||||
return
|
return
|
||||||
@@ -194,7 +197,16 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
|||||||
delta := utils.Btoi(sprites) + utils.Btoi(previews) + utils.Btoi(markers) + utils.Btoi(transcodes)
|
delta := utils.Btoi(sprites) + utils.Btoi(previews) + utils.Btoi(markers) + utils.Btoi(transcodes)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
s.Status.Progress = 0
|
s.Status.Progress = 0
|
||||||
total := len(scenes)
|
lenScenes := len(scenes)
|
||||||
|
total := lenScenes
|
||||||
|
if thumbnails {
|
||||||
|
galleries, err = qg.All()
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("failed to get galleries for generate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total += len(galleries)
|
||||||
|
}
|
||||||
|
|
||||||
if s.Status.stopping {
|
if s.Status.stopping {
|
||||||
logger.Info("Stopping due to user request")
|
logger.Info("Stopping due to user request")
|
||||||
@@ -248,6 +260,28 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
|||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if thumbnails {
|
||||||
|
logger.Infof("Generating thumbnails for the galleries")
|
||||||
|
for i, gallery := range galleries {
|
||||||
|
s.Status.setProgress(lenScenes+i, total)
|
||||||
|
if s.Status.stopping {
|
||||||
|
logger.Info("Stopping due to user request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if gallery == nil {
|
||||||
|
logger.Errorf("nil gallery, skipping generate")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
task := GenerateGthumbsTask{Gallery: *gallery}
|
||||||
|
go task.Start(&wg)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.Infof("Generate finished")
|
logger.Infof("Generate finished")
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package paths
|
package paths
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/stashapp/stash/pkg/manager/config"
|
"github.com/stashapp/stash/pkg/manager/config"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type galleryPaths struct{}
|
type galleryPaths struct{}
|
||||||
|
|
||||||
|
const thumbDir = "gthumbs"
|
||||||
|
const thumbDirDepth int = 2
|
||||||
|
const thumbDirLength int = 2 // thumbDirDepth * thumbDirLength must be smaller than the length of checksum
|
||||||
|
|
||||||
func newGalleryPaths() *galleryPaths {
|
func newGalleryPaths() *galleryPaths {
|
||||||
return &galleryPaths{}
|
return &galleryPaths{}
|
||||||
}
|
}
|
||||||
@@ -15,6 +21,19 @@ func (gp *galleryPaths) GetExtractedPath(checksum string) string {
|
|||||||
return filepath.Join(config.GetCachePath(), checksum)
|
return filepath.Join(config.GetCachePath(), checksum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetGthumbCache() string {
|
||||||
|
return filepath.Join(config.GetCachePath(), thumbDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGthumbDir(checksum string) string {
|
||||||
|
return filepath.Join(config.GetCachePath(), thumbDir, utils.GetIntraDir(checksum, thumbDirDepth, thumbDirLength), checksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGthumbPath(checksum string, index int, width int) string {
|
||||||
|
fname := fmt.Sprintf("%s_%d_%d.jpg", checksum, index, width)
|
||||||
|
return filepath.Join(config.GetCachePath(), thumbDir, utils.GetIntraDir(checksum, thumbDirDepth, thumbDirLength), checksum, fname)
|
||||||
|
}
|
||||||
|
|
||||||
func (gp *galleryPaths) GetExtractedFilePath(checksum string, fileName string) string {
|
func (gp *galleryPaths) GetExtractedFilePath(checksum string, fileName string) string {
|
||||||
return filepath.Join(config.GetCachePath(), checksum, fileName)
|
return filepath.Join(config.GetCachePath(), checksum, fileName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/manager/config"
|
"github.com/stashapp/stash/pkg/manager/config"
|
||||||
|
"github.com/stashapp/stash/pkg/manager/paths"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,13 +55,13 @@ func (t *CleanTask) deleteScene(sceneID int) {
|
|||||||
err = DestroyScene(sceneID, tx)
|
err = DestroyScene(sceneID, tx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infof("Error deleting scene from database: %s", err.Error())
|
logger.Errorf("Error deleting scene from database: %s", err.Error())
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
logger.Infof("Error deleting scene from database: %s", err.Error())
|
logger.Errorf("Error deleting scene from database: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,15 +76,20 @@ func (t *CleanTask) deleteGallery(galleryID int) {
|
|||||||
err := qb.Destroy(galleryID, tx)
|
err := qb.Destroy(galleryID, tx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infof("Error deleting gallery from database: %s", err.Error())
|
logger.Errorf("Error deleting gallery from database: %s", err.Error())
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
logger.Infof("Error deleting gallery from database: %s", err.Error())
|
logger.Errorf("Error deleting gallery from database: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pathErr := os.RemoveAll(paths.GetGthumbDir(t.Gallery.Checksum)) // remove cache dir of gallery
|
||||||
|
if pathErr != nil {
|
||||||
|
logger.Errorf("Error deleting gallery directory from cache: %s", pathErr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CleanTask) fileExists(filename string) bool {
|
func (t *CleanTask) fileExists(filename string) bool {
|
||||||
|
|||||||
37
pkg/manager/task_generate_gallery_thumbs.go
Normal file
37
pkg/manager/task_generate_gallery_thumbs.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/manager/paths"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GenerateGthumbsTask struct {
|
||||||
|
Gallery models.Gallery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *GenerateGthumbsTask) Start(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
generated := 0
|
||||||
|
count := t.Gallery.ImageCount()
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
thumbPath := paths.GetGthumbPath(t.Gallery.Checksum, i, models.DefaultGthumbWidth)
|
||||||
|
exists, _ := utils.FileExists(thumbPath)
|
||||||
|
if exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := t.Gallery.GetThumbnail(i, models.DefaultGthumbWidth)
|
||||||
|
err := utils.WriteFile(thumbPath, data)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("error writing gallery thumbnail: %s", err)
|
||||||
|
} else {
|
||||||
|
generated++
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if generated > 0 {
|
||||||
|
logger.Infof("Generated %d thumbnails for %s", generated, t.Gallery.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,11 +34,16 @@ func (t *ScanTask) Start(wg *sync.WaitGroup) {
|
|||||||
func (t *ScanTask) scanGallery() {
|
func (t *ScanTask) scanGallery() {
|
||||||
qb := models.NewGalleryQueryBuilder()
|
qb := models.NewGalleryQueryBuilder()
|
||||||
gallery, _ := qb.FindByPath(t.FilePath)
|
gallery, _ := qb.FindByPath(t.FilePath)
|
||||||
|
|
||||||
if gallery != nil {
|
if gallery != nil {
|
||||||
// We already have this item in the database, keep going
|
// We already have this item in the database, keep going
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ok, err := utils.IsZipFileUncompressed(t.FilePath)
|
||||||
|
if err == nil && !ok {
|
||||||
|
logger.Warnf("%s is using above store (0) level compression.", t.FilePath)
|
||||||
|
}
|
||||||
checksum, err := t.calculateChecksum()
|
checksum, err := t.calculateChecksum()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err.Error())
|
logger.Error(err.Error())
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ type Gallery struct {
|
|||||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultGthumbWidth int = 200
|
||||||
|
|
||||||
func (g *Gallery) GetFiles(baseURL string) []*GalleryFilesType {
|
func (g *Gallery) GetFiles(baseURL string) []*GalleryFilesType {
|
||||||
var galleryFiles []*GalleryFilesType
|
var galleryFiles []*GalleryFilesType
|
||||||
filteredFiles, readCloser, err := g.listZipContents()
|
filteredFiles, readCloser, err := g.listZipContents()
|
||||||
@@ -152,3 +154,11 @@ func reorder(a []*zip.File, toFirst int) []*zip.File {
|
|||||||
}
|
}
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Gallery) ImageCount() int {
|
||||||
|
images, _, _ := g.listZipContents()
|
||||||
|
if images == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(images)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sceneTable = "scenes"
|
const sceneTable = "scenes"
|
||||||
@@ -202,7 +202,7 @@ func (qb *SceneQueryBuilder) SizeCount() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "0", err
|
return "0", err
|
||||||
}
|
}
|
||||||
return humanize.Bytes(sum), err
|
return utils.HumanizeBytes(sum), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *SceneQueryBuilder) CountByStudioID(studioID int) (int, error) {
|
func (qb *SceneQueryBuilder) CountByStudioID(studioID int) (int, error) {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/h2non/filetype"
|
"github.com/h2non/filetype"
|
||||||
"github.com/h2non/filetype/types"
|
"github.com/h2non/filetype/types"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -66,7 +68,12 @@ func EnsureDir(path string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveDir removes the given file path along with all of its contents
|
// EnsureDirAll will create a directory at the given path along with any necessary parents if they don't already exist
|
||||||
|
func EnsureDirAll(path string) error {
|
||||||
|
return os.MkdirAll(path, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveDir removes the given dir (if it exists) along with all of its contents
|
||||||
func RemoveDir(path string) error {
|
func RemoveDir(path string) error {
|
||||||
return os.RemoveAll(path)
|
return os.RemoveAll(path)
|
||||||
}
|
}
|
||||||
@@ -125,6 +132,75 @@ func GetHomeDirectory() string {
|
|||||||
return currentUser.HomeDir
|
return currentUser.HomeDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsZipFileUnmcompressed returns true if zip file in path is using 0 compression level
|
||||||
|
func IsZipFileUncompressed(path string) (bool, error) {
|
||||||
|
r, err := zip.OpenReader(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading zip file %s: %s\n", path, err)
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
defer r.Close()
|
||||||
|
for _, f := range r.File {
|
||||||
|
if f.FileInfo().IsDir() { // skip dirs, they always get store level compression
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return f.Method == 0, nil // check compression level of first actual file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// humanize code taken from https://github.com/dustin/go-humanize and adjusted
|
||||||
|
|
||||||
|
func logn(n, b float64) float64 {
|
||||||
|
return math.Log(n) / math.Log(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HumanizeBytes returns a human readable bytes string of a uint
|
||||||
|
func HumanizeBytes(s uint64) string {
|
||||||
|
sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
|
||||||
|
if s < 10 {
|
||||||
|
return fmt.Sprintf("%d B", s)
|
||||||
|
}
|
||||||
|
e := math.Floor(logn(float64(s), 1024))
|
||||||
|
suffix := sizes[int(e)]
|
||||||
|
val := math.Floor(float64(s)/math.Pow(1024, e)*10+0.5) / 10
|
||||||
|
f := "%.0f %s"
|
||||||
|
if val < 10 {
|
||||||
|
f = "%.1f %s"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(f, val, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile writes file to path creating parent directories if needed
|
||||||
|
func WriteFile(path string, file []byte) error {
|
||||||
|
pathErr := EnsureDirAll(filepath.Dir(path))
|
||||||
|
if pathErr != nil {
|
||||||
|
return fmt.Errorf("Cannot ensure path %s", pathErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ioutil.WriteFile(path, file, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Write error for thumbnail %s: %s ", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIntraDir returns a string that can be added to filepath.Join to implement directory depth, "" on error
|
||||||
|
//eg given a pattern of 0af63ce3c99162e9df23a997f62621c5 and a depth of 2 length of 3
|
||||||
|
//returns 0af/63c or 0af\63c ( dependin on os) that can be later used like this filepath.Join(directory, intradir, basename)
|
||||||
|
func GetIntraDir(pattern string, depth, length int) string {
|
||||||
|
if depth < 1 || length < 1 || (depth*length > len(pattern)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
intraDir := pattern[0:length] // depth 1 , get length number of characters from pattern
|
||||||
|
for i := 1; i < depth; i++ { // for every extra depth: move to the right of the pattern length positions, get length number of chars
|
||||||
|
intraDir = filepath.Join(intraDir, pattern[length*i:length*(i+1)]) // adding each time to intradir the extra characters with a filepath join
|
||||||
|
}
|
||||||
|
return intraDir
|
||||||
|
}
|
||||||
|
|
||||||
func GetDir(path string) string {
|
func GetDir(path string) string {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
path = GetHomeDirectory()
|
path = GetHomeDirectory()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
const [generatedPath, setGeneratedPath] = useState<string | undefined>(
|
const [generatedPath, setGeneratedPath] = useState<string | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
const [cachePath, setCachePath] = useState<string | undefined>(undefined);
|
||||||
const [maxTranscodeSize, setMaxTranscodeSize] = useState<
|
const [maxTranscodeSize, setMaxTranscodeSize] = useState<
|
||||||
GQL.StreamingResolutionEnum | undefined
|
GQL.StreamingResolutionEnum | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@@ -42,6 +43,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
stashes,
|
stashes,
|
||||||
databasePath,
|
databasePath,
|
||||||
generatedPath,
|
generatedPath,
|
||||||
|
cachePath,
|
||||||
maxTranscodeSize,
|
maxTranscodeSize,
|
||||||
maxStreamingTranscodeSize,
|
maxStreamingTranscodeSize,
|
||||||
forceMkv,
|
forceMkv,
|
||||||
@@ -65,6 +67,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
setStashes(conf.general.stashes ?? []);
|
setStashes(conf.general.stashes ?? []);
|
||||||
setDatabasePath(conf.general.databasePath);
|
setDatabasePath(conf.general.databasePath);
|
||||||
setGeneratedPath(conf.general.generatedPath);
|
setGeneratedPath(conf.general.generatedPath);
|
||||||
|
setCachePath(conf.general.cachePath);
|
||||||
setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined);
|
setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined);
|
||||||
setMaxStreamingTranscodeSize(
|
setMaxStreamingTranscodeSize(
|
||||||
conf.general.maxStreamingTranscodeSize ?? undefined
|
conf.general.maxStreamingTranscodeSize ?? undefined
|
||||||
@@ -213,6 +216,20 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="cache-path">
|
||||||
|
<h6>Cache Path</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
defaultValue={cachePath}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setCachePath(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Directory location of the cache
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h6>Excluded Patterns</h6>
|
<h6>Excluded Patterns</h6>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export const GenerateButton: React.FC = () => {
|
|||||||
const [sprites, setSprites] = useState(true);
|
const [sprites, setSprites] = useState(true);
|
||||||
const [previews, setPreviews] = useState(true);
|
const [previews, setPreviews] = useState(true);
|
||||||
const [markers, setMarkers] = useState(true);
|
const [markers, setMarkers] = useState(true);
|
||||||
const [transcodes, setTranscodes] = useState(true);
|
const [transcodes, setTranscodes] = useState(false);
|
||||||
|
const [thumbnails, setThumbnails] = useState(false);
|
||||||
|
|
||||||
async function onGenerate() {
|
async function onGenerate() {
|
||||||
try {
|
try {
|
||||||
@@ -17,6 +18,7 @@ export const GenerateButton: React.FC = () => {
|
|||||||
previews,
|
previews,
|
||||||
markers,
|
markers,
|
||||||
transcodes,
|
transcodes,
|
||||||
|
thumbnails,
|
||||||
});
|
});
|
||||||
Toast.success({ content: "Started generating" });
|
Toast.success({ content: "Started generating" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -51,6 +53,12 @@ export const GenerateButton: React.FC = () => {
|
|||||||
label="Transcodes (MP4 conversions of unsupported video formats)"
|
label="Transcodes (MP4 conversions of unsupported video formats)"
|
||||||
onChange={() => setTranscodes(!transcodes)}
|
onChange={() => setTranscodes(!transcodes)}
|
||||||
/>
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="thumbnail-task"
|
||||||
|
checked={thumbnails}
|
||||||
|
label="Gallery thumbnails (thumbnails for all the gallery images)"
|
||||||
|
onChange={() => setThumbnails(!thumbnails)}
|
||||||
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
21
vendor/github.com/dustin/go-humanize/.travis.yml
generated
vendored
21
vendor/github.com/dustin/go-humanize/.travis.yml
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.3.x
|
|
||||||
- 1.5.x
|
|
||||||
- 1.6.x
|
|
||||||
- 1.7.x
|
|
||||||
- 1.8.x
|
|
||||||
- 1.9.x
|
|
||||||
- master
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- go: master
|
|
||||||
fast_finish: true
|
|
||||||
install:
|
|
||||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
|
||||||
script:
|
|
||||||
- go get -t -v ./...
|
|
||||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
|
||||||
- go tool vet .
|
|
||||||
- go test -v -race ./...
|
|
||||||
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
<http://www.opensource.org/licenses/mit-license.php>
|
|
||||||
124
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
124
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
@@ -1,124 +0,0 @@
|
|||||||
# Humane Units [](https://travis-ci.org/dustin/go-humanize) [](https://godoc.org/github.com/dustin/go-humanize)
|
|
||||||
|
|
||||||
Just a few functions for helping humanize times and sizes.
|
|
||||||
|
|
||||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
|
||||||
`"github.com/dustin/go-humanize"`, use it as `humanize`.
|
|
||||||
|
|
||||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
|
|
||||||
complete documentation.
|
|
||||||
|
|
||||||
## Sizes
|
|
||||||
|
|
||||||
This lets you take numbers like `82854982` and convert them to useful
|
|
||||||
strings like, `83 MB` or `79 MiB` (whichever you prefer).
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Times
|
|
||||||
|
|
||||||
This lets you take a `time.Time` and spit it out in relative terms.
|
|
||||||
For example, `12 seconds ago` or `3 days from now`.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
|
|
||||||
```
|
|
||||||
|
|
||||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
|
||||||
conversation one day. It's pretty neat.
|
|
||||||
|
|
||||||
## Ordinals
|
|
||||||
|
|
||||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
|
||||||
to label ordinals.
|
|
||||||
|
|
||||||
0 -> 0th
|
|
||||||
1 -> 1st
|
|
||||||
2 -> 2nd
|
|
||||||
3 -> 3rd
|
|
||||||
4 -> 4th
|
|
||||||
[...]
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Commas
|
|
||||||
|
|
||||||
Want to shove commas into numbers? Be my guest.
|
|
||||||
|
|
||||||
0 -> 0
|
|
||||||
100 -> 100
|
|
||||||
1000 -> 1,000
|
|
||||||
1000000000 -> 1,000,000,000
|
|
||||||
-100000 -> -100,000
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ftoa
|
|
||||||
|
|
||||||
Nicer float64 formatter that removes trailing zeros.
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Printf("%f", 2.24) // 2.240000
|
|
||||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
|
||||||
fmt.Printf("%f", 2.0) // 2.000000
|
|
||||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
|
||||||
```
|
|
||||||
|
|
||||||
## SI notation
|
|
||||||
|
|
||||||
Format numbers with [SI notation][sinotation].
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
humanize.SI(0.00000000223, "M") // 2.23 nM
|
|
||||||
```
|
|
||||||
|
|
||||||
## English-specific functions
|
|
||||||
|
|
||||||
The following functions are in the `humanize/english` subpackage.
|
|
||||||
|
|
||||||
### Plurals
|
|
||||||
|
|
||||||
Simple English pluralization
|
|
||||||
|
|
||||||
```go
|
|
||||||
english.PluralWord(1, "object", "") // object
|
|
||||||
english.PluralWord(42, "object", "") // objects
|
|
||||||
english.PluralWord(2, "bus", "") // buses
|
|
||||||
english.PluralWord(99, "locus", "loci") // loci
|
|
||||||
|
|
||||||
english.Plural(1, "object", "") // 1 object
|
|
||||||
english.Plural(42, "object", "") // 42 objects
|
|
||||||
english.Plural(2, "bus", "") // 2 buses
|
|
||||||
english.Plural(99, "locus", "loci") // 99 loci
|
|
||||||
```
|
|
||||||
|
|
||||||
### Word series
|
|
||||||
|
|
||||||
Format comma-separated words lists with conjuctions:
|
|
||||||
|
|
||||||
```go
|
|
||||||
english.WordSeries([]string{"foo"}, "and") // foo
|
|
||||||
english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar
|
|
||||||
english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz
|
|
||||||
|
|
||||||
english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz
|
|
||||||
```
|
|
||||||
|
|
||||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
|
||||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
|
||||||
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
@@ -1,31 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
)
|
|
||||||
|
|
||||||
// order of magnitude (to a max order)
|
|
||||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
|
||||||
mag := 0
|
|
||||||
m := &big.Int{}
|
|
||||||
for n.Cmp(b) >= 0 {
|
|
||||||
n.DivMod(n, b, m)
|
|
||||||
mag++
|
|
||||||
if mag == maxmag && maxmag >= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
|
||||||
}
|
|
||||||
|
|
||||||
// total order of magnitude
|
|
||||||
// (same as above, but with no upper limit)
|
|
||||||
func oom(n, b *big.Int) (float64, int) {
|
|
||||||
mag := 0
|
|
||||||
m := &big.Int{}
|
|
||||||
for n.Cmp(b) >= 0 {
|
|
||||||
n.DivMod(n, b, m)
|
|
||||||
mag++
|
|
||||||
}
|
|
||||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
|
||||||
}
|
|
||||||
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
@@ -1,173 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
bigIECExp = big.NewInt(1024)
|
|
||||||
|
|
||||||
// BigByte is one byte in bit.Ints
|
|
||||||
BigByte = big.NewInt(1)
|
|
||||||
// BigKiByte is 1,024 bytes in bit.Ints
|
|
||||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
|
||||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
|
||||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
|
||||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
|
||||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
|
||||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
|
||||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
|
||||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
|
||||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
|
||||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
|
||||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
|
||||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
|
||||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
|
||||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
|
||||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
bigSIExp = big.NewInt(1000)
|
|
||||||
|
|
||||||
// BigSIByte is one SI byte in big.Ints
|
|
||||||
BigSIByte = big.NewInt(1)
|
|
||||||
// BigKByte is 1,000 SI bytes in big.Ints
|
|
||||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
|
||||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
|
||||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
|
||||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
|
||||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
|
||||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
|
||||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
|
||||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
|
||||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
|
||||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
|
||||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
|
||||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
|
||||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
|
||||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
|
||||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
|
||||||
)
|
|
||||||
|
|
||||||
var bigBytesSizeTable = map[string]*big.Int{
|
|
||||||
"b": BigByte,
|
|
||||||
"kib": BigKiByte,
|
|
||||||
"kb": BigKByte,
|
|
||||||
"mib": BigMiByte,
|
|
||||||
"mb": BigMByte,
|
|
||||||
"gib": BigGiByte,
|
|
||||||
"gb": BigGByte,
|
|
||||||
"tib": BigTiByte,
|
|
||||||
"tb": BigTByte,
|
|
||||||
"pib": BigPiByte,
|
|
||||||
"pb": BigPByte,
|
|
||||||
"eib": BigEiByte,
|
|
||||||
"eb": BigEByte,
|
|
||||||
"zib": BigZiByte,
|
|
||||||
"zb": BigZByte,
|
|
||||||
"yib": BigYiByte,
|
|
||||||
"yb": BigYByte,
|
|
||||||
// Without suffix
|
|
||||||
"": BigByte,
|
|
||||||
"ki": BigKiByte,
|
|
||||||
"k": BigKByte,
|
|
||||||
"mi": BigMiByte,
|
|
||||||
"m": BigMByte,
|
|
||||||
"gi": BigGiByte,
|
|
||||||
"g": BigGByte,
|
|
||||||
"ti": BigTiByte,
|
|
||||||
"t": BigTByte,
|
|
||||||
"pi": BigPiByte,
|
|
||||||
"p": BigPByte,
|
|
||||||
"ei": BigEiByte,
|
|
||||||
"e": BigEByte,
|
|
||||||
"z": BigZByte,
|
|
||||||
"zi": BigZiByte,
|
|
||||||
"y": BigYByte,
|
|
||||||
"yi": BigYiByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
var ten = big.NewInt(10)
|
|
||||||
|
|
||||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
|
||||||
if s.Cmp(ten) < 0 {
|
|
||||||
return fmt.Sprintf("%d B", s)
|
|
||||||
}
|
|
||||||
c := (&big.Int{}).Set(s)
|
|
||||||
val, mag := oomm(c, base, len(sizes)-1)
|
|
||||||
suffix := sizes[mag]
|
|
||||||
f := "%.0f %s"
|
|
||||||
if val < 10 {
|
|
||||||
f = "%.1f %s"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(f, val, suffix)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// BigBytes produces a human readable representation of an SI size.
|
|
||||||
//
|
|
||||||
// See also: ParseBigBytes.
|
|
||||||
//
|
|
||||||
// BigBytes(82854982) -> 83 MB
|
|
||||||
func BigBytes(s *big.Int) string {
|
|
||||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
|
||||||
return humanateBigBytes(s, bigSIExp, sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BigIBytes produces a human readable representation of an IEC size.
|
|
||||||
//
|
|
||||||
// See also: ParseBigBytes.
|
|
||||||
//
|
|
||||||
// BigIBytes(82854982) -> 79 MiB
|
|
||||||
func BigIBytes(s *big.Int) string {
|
|
||||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
|
||||||
return humanateBigBytes(s, bigIECExp, sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseBigBytes parses a string representation of bytes into the number
|
|
||||||
// of bytes it represents.
|
|
||||||
//
|
|
||||||
// See also: BigBytes, BigIBytes.
|
|
||||||
//
|
|
||||||
// ParseBigBytes("42 MB") -> 42000000, nil
|
|
||||||
// ParseBigBytes("42 mib") -> 44040192, nil
|
|
||||||
func ParseBigBytes(s string) (*big.Int, error) {
|
|
||||||
lastDigit := 0
|
|
||||||
hasComma := false
|
|
||||||
for _, r := range s {
|
|
||||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if r == ',' {
|
|
||||||
hasComma = true
|
|
||||||
}
|
|
||||||
lastDigit++
|
|
||||||
}
|
|
||||||
|
|
||||||
num := s[:lastDigit]
|
|
||||||
if hasComma {
|
|
||||||
num = strings.Replace(num, ",", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val := &big.Rat{}
|
|
||||||
_, err := fmt.Sscanf(num, "%f", val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
|
||||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
|
||||||
mv := (&big.Rat{}).SetInt(m)
|
|
||||||
val.Mul(val, mv)
|
|
||||||
rv := &big.Int{}
|
|
||||||
rv.Div(val.Num(), val.Denom())
|
|
||||||
return rv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
|
||||||
}
|
|
||||||
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
@@ -1,143 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IEC Sizes.
|
|
||||||
// kibis of bits
|
|
||||||
const (
|
|
||||||
Byte = 1 << (iota * 10)
|
|
||||||
KiByte
|
|
||||||
MiByte
|
|
||||||
GiByte
|
|
||||||
TiByte
|
|
||||||
PiByte
|
|
||||||
EiByte
|
|
||||||
)
|
|
||||||
|
|
||||||
// SI Sizes.
|
|
||||||
const (
|
|
||||||
IByte = 1
|
|
||||||
KByte = IByte * 1000
|
|
||||||
MByte = KByte * 1000
|
|
||||||
GByte = MByte * 1000
|
|
||||||
TByte = GByte * 1000
|
|
||||||
PByte = TByte * 1000
|
|
||||||
EByte = PByte * 1000
|
|
||||||
)
|
|
||||||
|
|
||||||
var bytesSizeTable = map[string]uint64{
|
|
||||||
"b": Byte,
|
|
||||||
"kib": KiByte,
|
|
||||||
"kb": KByte,
|
|
||||||
"mib": MiByte,
|
|
||||||
"mb": MByte,
|
|
||||||
"gib": GiByte,
|
|
||||||
"gb": GByte,
|
|
||||||
"tib": TiByte,
|
|
||||||
"tb": TByte,
|
|
||||||
"pib": PiByte,
|
|
||||||
"pb": PByte,
|
|
||||||
"eib": EiByte,
|
|
||||||
"eb": EByte,
|
|
||||||
// Without suffix
|
|
||||||
"": Byte,
|
|
||||||
"ki": KiByte,
|
|
||||||
"k": KByte,
|
|
||||||
"mi": MiByte,
|
|
||||||
"m": MByte,
|
|
||||||
"gi": GiByte,
|
|
||||||
"g": GByte,
|
|
||||||
"ti": TiByte,
|
|
||||||
"t": TByte,
|
|
||||||
"pi": PiByte,
|
|
||||||
"p": PByte,
|
|
||||||
"ei": EiByte,
|
|
||||||
"e": EByte,
|
|
||||||
}
|
|
||||||
|
|
||||||
func logn(n, b float64) float64 {
|
|
||||||
return math.Log(n) / math.Log(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
|
||||||
if s < 10 {
|
|
||||||
return fmt.Sprintf("%d B", s)
|
|
||||||
}
|
|
||||||
e := math.Floor(logn(float64(s), base))
|
|
||||||
suffix := sizes[int(e)]
|
|
||||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
|
||||||
f := "%.0f %s"
|
|
||||||
if val < 10 {
|
|
||||||
f = "%.1f %s"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(f, val, suffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes produces a human readable representation of an SI size.
|
|
||||||
//
|
|
||||||
// See also: ParseBytes.
|
|
||||||
//
|
|
||||||
// Bytes(82854982) -> 83 MB
|
|
||||||
func Bytes(s uint64) string {
|
|
||||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
|
||||||
return humanateBytes(s, 1000, sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IBytes produces a human readable representation of an IEC size.
|
|
||||||
//
|
|
||||||
// See also: ParseBytes.
|
|
||||||
//
|
|
||||||
// IBytes(82854982) -> 79 MiB
|
|
||||||
func IBytes(s uint64) string {
|
|
||||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
|
||||||
return humanateBytes(s, 1024, sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseBytes parses a string representation of bytes into the number
|
|
||||||
// of bytes it represents.
|
|
||||||
//
|
|
||||||
// See Also: Bytes, IBytes.
|
|
||||||
//
|
|
||||||
// ParseBytes("42 MB") -> 42000000, nil
|
|
||||||
// ParseBytes("42 mib") -> 44040192, nil
|
|
||||||
func ParseBytes(s string) (uint64, error) {
|
|
||||||
lastDigit := 0
|
|
||||||
hasComma := false
|
|
||||||
for _, r := range s {
|
|
||||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if r == ',' {
|
|
||||||
hasComma = true
|
|
||||||
}
|
|
||||||
lastDigit++
|
|
||||||
}
|
|
||||||
|
|
||||||
num := s[:lastDigit]
|
|
||||||
if hasComma {
|
|
||||||
num = strings.Replace(num, ",", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := strconv.ParseFloat(num, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
|
||||||
if m, ok := bytesSizeTable[extra]; ok {
|
|
||||||
f *= float64(m)
|
|
||||||
if f >= math.MaxUint64 {
|
|
||||||
return 0, fmt.Errorf("too large: %v", s)
|
|
||||||
}
|
|
||||||
return uint64(f), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
|
||||||
}
|
|
||||||
116
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
116
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
@@ -1,116 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Comma produces a string form of the given number in base 10 with
|
|
||||||
// commas after every three orders of magnitude.
|
|
||||||
//
|
|
||||||
// e.g. Comma(834142) -> 834,142
|
|
||||||
func Comma(v int64) string {
|
|
||||||
sign := ""
|
|
||||||
|
|
||||||
// Min int64 can't be negated to a usable value, so it has to be special cased.
|
|
||||||
if v == math.MinInt64 {
|
|
||||||
return "-9,223,372,036,854,775,808"
|
|
||||||
}
|
|
||||||
|
|
||||||
if v < 0 {
|
|
||||||
sign = "-"
|
|
||||||
v = 0 - v
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := []string{"", "", "", "", "", "", ""}
|
|
||||||
j := len(parts) - 1
|
|
||||||
|
|
||||||
for v > 999 {
|
|
||||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
|
||||||
switch len(parts[j]) {
|
|
||||||
case 2:
|
|
||||||
parts[j] = "0" + parts[j]
|
|
||||||
case 1:
|
|
||||||
parts[j] = "00" + parts[j]
|
|
||||||
}
|
|
||||||
v = v / 1000
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
parts[j] = strconv.Itoa(int(v))
|
|
||||||
return sign + strings.Join(parts[j:], ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commaf produces a string form of the given number in base 10 with
|
|
||||||
// commas after every three orders of magnitude.
|
|
||||||
//
|
|
||||||
// e.g. Commaf(834142.32) -> 834,142.32
|
|
||||||
func Commaf(v float64) string {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
if v < 0 {
|
|
||||||
buf.Write([]byte{'-'})
|
|
||||||
v = 0 - v
|
|
||||||
}
|
|
||||||
|
|
||||||
comma := []byte{','}
|
|
||||||
|
|
||||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
|
||||||
pos := 0
|
|
||||||
if len(parts[0])%3 != 0 {
|
|
||||||
pos += len(parts[0]) % 3
|
|
||||||
buf.WriteString(parts[0][:pos])
|
|
||||||
buf.Write(comma)
|
|
||||||
}
|
|
||||||
for ; pos < len(parts[0]); pos += 3 {
|
|
||||||
buf.WriteString(parts[0][pos : pos+3])
|
|
||||||
buf.Write(comma)
|
|
||||||
}
|
|
||||||
buf.Truncate(buf.Len() - 1)
|
|
||||||
|
|
||||||
if len(parts) > 1 {
|
|
||||||
buf.Write([]byte{'.'})
|
|
||||||
buf.WriteString(parts[1])
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommafWithDigits works like the Commaf but limits the resulting
|
|
||||||
// string to the given number of decimal places.
|
|
||||||
//
|
|
||||||
// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3
|
|
||||||
func CommafWithDigits(f float64, decimals int) string {
|
|
||||||
return stripTrailingDigits(Commaf(f), decimals)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BigComma produces a string form of the given big.Int in base 10
|
|
||||||
// with commas after every three orders of magnitude.
|
|
||||||
func BigComma(b *big.Int) string {
|
|
||||||
sign := ""
|
|
||||||
if b.Sign() < 0 {
|
|
||||||
sign = "-"
|
|
||||||
b.Abs(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
athousand := big.NewInt(1000)
|
|
||||||
c := (&big.Int{}).Set(b)
|
|
||||||
_, m := oom(c, athousand)
|
|
||||||
parts := make([]string, m+1)
|
|
||||||
j := len(parts) - 1
|
|
||||||
|
|
||||||
mod := &big.Int{}
|
|
||||||
for b.Cmp(athousand) >= 0 {
|
|
||||||
b.DivMod(b, athousand, mod)
|
|
||||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
|
||||||
switch len(parts[j]) {
|
|
||||||
case 2:
|
|
||||||
parts[j] = "0" + parts[j]
|
|
||||||
case 1:
|
|
||||||
parts[j] = "00" + parts[j]
|
|
||||||
}
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
|
||||||
return sign + strings.Join(parts[j:], ",")
|
|
||||||
}
|
|
||||||
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
@@ -1,40 +0,0 @@
|
|||||||
// +build go1.6
|
|
||||||
|
|
||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BigCommaf produces a string form of the given big.Float in base 10
|
|
||||||
// with commas after every three orders of magnitude.
|
|
||||||
func BigCommaf(v *big.Float) string {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
if v.Sign() < 0 {
|
|
||||||
buf.Write([]byte{'-'})
|
|
||||||
v.Abs(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
comma := []byte{','}
|
|
||||||
|
|
||||||
parts := strings.Split(v.Text('f', -1), ".")
|
|
||||||
pos := 0
|
|
||||||
if len(parts[0])%3 != 0 {
|
|
||||||
pos += len(parts[0]) % 3
|
|
||||||
buf.WriteString(parts[0][:pos])
|
|
||||||
buf.Write(comma)
|
|
||||||
}
|
|
||||||
for ; pos < len(parts[0]); pos += 3 {
|
|
||||||
buf.WriteString(parts[0][pos : pos+3])
|
|
||||||
buf.Write(comma)
|
|
||||||
}
|
|
||||||
buf.Truncate(buf.Len() - 1)
|
|
||||||
|
|
||||||
if len(parts) > 1 {
|
|
||||||
buf.Write([]byte{'.'})
|
|
||||||
buf.WriteString(parts[1])
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
46
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
46
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
@@ -1,46 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func stripTrailingZeros(s string) string {
|
|
||||||
offset := len(s) - 1
|
|
||||||
for offset > 0 {
|
|
||||||
if s[offset] == '.' {
|
|
||||||
offset--
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if s[offset] != '0' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
offset--
|
|
||||||
}
|
|
||||||
return s[:offset+1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripTrailingDigits(s string, digits int) string {
|
|
||||||
if i := strings.Index(s, "."); i >= 0 {
|
|
||||||
if digits <= 0 {
|
|
||||||
return s[:i]
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
if i+digits >= len(s) {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s[:i+digits]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ftoa converts a float to a string with no trailing zeros.
|
|
||||||
func Ftoa(num float64) string {
|
|
||||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
|
||||||
}
|
|
||||||
|
|
||||||
// FtoaWithDigits converts a float to a string but limits the resulting string
|
|
||||||
// to the given number of decimal places, and no trailing zeros.
|
|
||||||
func FtoaWithDigits(num float64, digits int) string {
|
|
||||||
return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits))
|
|
||||||
}
|
|
||||||
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
|
||||||
|
|
||||||
Durations can be turned into strings such as "3 days ago", numbers
|
|
||||||
representing sizes like 82854982 into useful strings like, "83 MB" or
|
|
||||||
"79 MiB" (whichever you prefer).
|
|
||||||
*/
|
|
||||||
package humanize
|
|
||||||
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
@@ -1,192 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
/*
|
|
||||||
Slightly adapted from the source to fit go-humanize.
|
|
||||||
|
|
||||||
Author: https://github.com/gorhill
|
|
||||||
Source: https://gist.github.com/gorhill/5285193
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
renderFloatPrecisionMultipliers = [...]float64{
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
100,
|
|
||||||
1000,
|
|
||||||
10000,
|
|
||||||
100000,
|
|
||||||
1000000,
|
|
||||||
10000000,
|
|
||||||
100000000,
|
|
||||||
1000000000,
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFloatPrecisionRounders = [...]float64{
|
|
||||||
0.5,
|
|
||||||
0.05,
|
|
||||||
0.005,
|
|
||||||
0.0005,
|
|
||||||
0.00005,
|
|
||||||
0.000005,
|
|
||||||
0.0000005,
|
|
||||||
0.00000005,
|
|
||||||
0.000000005,
|
|
||||||
0.0000000005,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
|
||||||
// * thousands separator
|
|
||||||
// * decimal separator
|
|
||||||
// * decimal precision
|
|
||||||
//
|
|
||||||
// Usage: s := RenderFloat(format, n)
|
|
||||||
// The format parameter tells how to render the number n.
|
|
||||||
//
|
|
||||||
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
|
||||||
//
|
|
||||||
// Examples of format strings, given n = 12345.6789:
|
|
||||||
// "#,###.##" => "12,345.67"
|
|
||||||
// "#,###." => "12,345"
|
|
||||||
// "#,###" => "12345,678"
|
|
||||||
// "#\u202F###,##" => "12 345,68"
|
|
||||||
// "#.###,###### => 12.345,678900
|
|
||||||
// "" (aka default format) => 12,345.67
|
|
||||||
//
|
|
||||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
|
||||||
// There is also a version for integer number, FormatInteger(),
|
|
||||||
// which is convenient for calls within template.
|
|
||||||
func FormatFloat(format string, n float64) string {
|
|
||||||
// Special cases:
|
|
||||||
// NaN = "NaN"
|
|
||||||
// +Inf = "+Infinity"
|
|
||||||
// -Inf = "-Infinity"
|
|
||||||
if math.IsNaN(n) {
|
|
||||||
return "NaN"
|
|
||||||
}
|
|
||||||
if n > math.MaxFloat64 {
|
|
||||||
return "Infinity"
|
|
||||||
}
|
|
||||||
if n < -math.MaxFloat64 {
|
|
||||||
return "-Infinity"
|
|
||||||
}
|
|
||||||
|
|
||||||
// default format
|
|
||||||
precision := 2
|
|
||||||
decimalStr := "."
|
|
||||||
thousandStr := ","
|
|
||||||
positiveStr := ""
|
|
||||||
negativeStr := "-"
|
|
||||||
|
|
||||||
if len(format) > 0 {
|
|
||||||
format := []rune(format)
|
|
||||||
|
|
||||||
// If there is an explicit format directive,
|
|
||||||
// then default values are these:
|
|
||||||
precision = 9
|
|
||||||
thousandStr = ""
|
|
||||||
|
|
||||||
// collect indices of meaningful formatting directives
|
|
||||||
formatIndx := []int{}
|
|
||||||
for i, char := range format {
|
|
||||||
if char != '#' && char != '0' {
|
|
||||||
formatIndx = append(formatIndx, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(formatIndx) > 0 {
|
|
||||||
// Directive at index 0:
|
|
||||||
// Must be a '+'
|
|
||||||
// Raise an error if not the case
|
|
||||||
// index: 0123456789
|
|
||||||
// +0.000,000
|
|
||||||
// +000,000.0
|
|
||||||
// +0000.00
|
|
||||||
// +0000
|
|
||||||
if formatIndx[0] == 0 {
|
|
||||||
if format[formatIndx[0]] != '+' {
|
|
||||||
panic("RenderFloat(): invalid positive sign directive")
|
|
||||||
}
|
|
||||||
positiveStr = "+"
|
|
||||||
formatIndx = formatIndx[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Two directives:
|
|
||||||
// First is thousands separator
|
|
||||||
// Raise an error if not followed by 3-digit
|
|
||||||
// 0123456789
|
|
||||||
// 0.000,000
|
|
||||||
// 000,000.00
|
|
||||||
if len(formatIndx) == 2 {
|
|
||||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
|
||||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
|
||||||
}
|
|
||||||
thousandStr = string(format[formatIndx[0]])
|
|
||||||
formatIndx = formatIndx[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// One directive:
|
|
||||||
// Directive is decimal separator
|
|
||||||
// The number of digit-specifier following the separator indicates wanted precision
|
|
||||||
// 0123456789
|
|
||||||
// 0.00
|
|
||||||
// 000,0000
|
|
||||||
if len(formatIndx) == 1 {
|
|
||||||
decimalStr = string(format[formatIndx[0]])
|
|
||||||
precision = len(format) - formatIndx[0] - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate sign part
|
|
||||||
var signStr string
|
|
||||||
if n >= 0.000000001 {
|
|
||||||
signStr = positiveStr
|
|
||||||
} else if n <= -0.000000001 {
|
|
||||||
signStr = negativeStr
|
|
||||||
n = -n
|
|
||||||
} else {
|
|
||||||
signStr = ""
|
|
||||||
n = 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// split number into integer and fractional parts
|
|
||||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
|
||||||
|
|
||||||
// generate integer part string
|
|
||||||
intStr := strconv.FormatInt(int64(intf), 10)
|
|
||||||
|
|
||||||
// add thousand separator if required
|
|
||||||
if len(thousandStr) > 0 {
|
|
||||||
for i := len(intStr); i > 3; {
|
|
||||||
i -= 3
|
|
||||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no fractional part, we can leave now
|
|
||||||
if precision == 0 {
|
|
||||||
return signStr + intStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate fractional part
|
|
||||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
|
||||||
// may need padding
|
|
||||||
if len(fracStr) < precision {
|
|
||||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
|
||||||
}
|
|
||||||
|
|
||||||
return signStr + intStr + decimalStr + fracStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatInteger produces a formatted number as string.
|
|
||||||
// See FormatFloat.
|
|
||||||
func FormatInteger(format string, n int) string {
|
|
||||||
return FormatFloat(format, float64(n))
|
|
||||||
}
|
|
||||||
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
@@ -1,25 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
// Ordinal gives you the input number in a rank/ordinal format.
|
|
||||||
//
|
|
||||||
// Ordinal(3) -> 3rd
|
|
||||||
func Ordinal(x int) string {
|
|
||||||
suffix := "th"
|
|
||||||
switch x % 10 {
|
|
||||||
case 1:
|
|
||||||
if x%100 != 11 {
|
|
||||||
suffix = "st"
|
|
||||||
}
|
|
||||||
case 2:
|
|
||||||
if x%100 != 12 {
|
|
||||||
suffix = "nd"
|
|
||||||
}
|
|
||||||
case 3:
|
|
||||||
if x%100 != 13 {
|
|
||||||
suffix = "rd"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strconv.Itoa(x) + suffix
|
|
||||||
}
|
|
||||||
123
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
123
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
@@ -1,123 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"math"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var siPrefixTable = map[float64]string{
|
|
||||||
-24: "y", // yocto
|
|
||||||
-21: "z", // zepto
|
|
||||||
-18: "a", // atto
|
|
||||||
-15: "f", // femto
|
|
||||||
-12: "p", // pico
|
|
||||||
-9: "n", // nano
|
|
||||||
-6: "µ", // micro
|
|
||||||
-3: "m", // milli
|
|
||||||
0: "",
|
|
||||||
3: "k", // kilo
|
|
||||||
6: "M", // mega
|
|
||||||
9: "G", // giga
|
|
||||||
12: "T", // tera
|
|
||||||
15: "P", // peta
|
|
||||||
18: "E", // exa
|
|
||||||
21: "Z", // zetta
|
|
||||||
24: "Y", // yotta
|
|
||||||
}
|
|
||||||
|
|
||||||
var revSIPrefixTable = revfmap(siPrefixTable)
|
|
||||||
|
|
||||||
// revfmap reverses the map and precomputes the power multiplier
|
|
||||||
func revfmap(in map[float64]string) map[string]float64 {
|
|
||||||
rv := map[string]float64{}
|
|
||||||
for k, v := range in {
|
|
||||||
rv[v] = math.Pow(10, k)
|
|
||||||
}
|
|
||||||
return rv
|
|
||||||
}
|
|
||||||
|
|
||||||
var riParseRegex *regexp.Regexp
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
ri := `^([\-0-9.]+)\s?([`
|
|
||||||
for _, v := range siPrefixTable {
|
|
||||||
ri += v
|
|
||||||
}
|
|
||||||
ri += `]?)(.*)`
|
|
||||||
|
|
||||||
riParseRegex = regexp.MustCompile(ri)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ComputeSI finds the most appropriate SI prefix for the given number
|
|
||||||
// and returns the prefix along with the value adjusted to be within
|
|
||||||
// that prefix.
|
|
||||||
//
|
|
||||||
// See also: SI, ParseSI.
|
|
||||||
//
|
|
||||||
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
|
|
||||||
func ComputeSI(input float64) (float64, string) {
|
|
||||||
if input == 0 {
|
|
||||||
return 0, ""
|
|
||||||
}
|
|
||||||
mag := math.Abs(input)
|
|
||||||
exponent := math.Floor(logn(mag, 10))
|
|
||||||
exponent = math.Floor(exponent/3) * 3
|
|
||||||
|
|
||||||
value := mag / math.Pow(10, exponent)
|
|
||||||
|
|
||||||
// Handle special case where value is exactly 1000.0
|
|
||||||
// Should return 1 M instead of 1000 k
|
|
||||||
if value == 1000.0 {
|
|
||||||
exponent += 3
|
|
||||||
value = mag / math.Pow(10, exponent)
|
|
||||||
}
|
|
||||||
|
|
||||||
value = math.Copysign(value, input)
|
|
||||||
|
|
||||||
prefix := siPrefixTable[exponent]
|
|
||||||
return value, prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
// SI returns a string with default formatting.
|
|
||||||
//
|
|
||||||
// SI uses Ftoa to format float value, removing trailing zeros.
|
|
||||||
//
|
|
||||||
// See also: ComputeSI, ParseSI.
|
|
||||||
//
|
|
||||||
// e.g. SI(1000000, "B") -> 1 MB
|
|
||||||
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
|
|
||||||
func SI(input float64, unit string) string {
|
|
||||||
value, prefix := ComputeSI(input)
|
|
||||||
return Ftoa(value) + " " + prefix + unit
|
|
||||||
}
|
|
||||||
|
|
||||||
// SIWithDigits works like SI but limits the resulting string to the
|
|
||||||
// given number of decimal places.
|
|
||||||
//
|
|
||||||
// e.g. SIWithDigits(1000000, 0, "B") -> 1 MB
|
|
||||||
// e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF
|
|
||||||
func SIWithDigits(input float64, decimals int, unit string) string {
|
|
||||||
value, prefix := ComputeSI(input)
|
|
||||||
return FtoaWithDigits(value, decimals) + " " + prefix + unit
|
|
||||||
}
|
|
||||||
|
|
||||||
var errInvalid = errors.New("invalid input")
|
|
||||||
|
|
||||||
// ParseSI parses an SI string back into the number and unit.
|
|
||||||
//
|
|
||||||
// See also: SI, ComputeSI.
|
|
||||||
//
|
|
||||||
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
|
|
||||||
func ParseSI(input string) (float64, string, error) {
|
|
||||||
found := riParseRegex.FindStringSubmatch(input)
|
|
||||||
if len(found) != 4 {
|
|
||||||
return 0, "", errInvalid
|
|
||||||
}
|
|
||||||
mag := revSIPrefixTable[found[2]]
|
|
||||||
unit := found[3]
|
|
||||||
|
|
||||||
base, err := strconv.ParseFloat(found[1], 64)
|
|
||||||
return base * mag, unit, err
|
|
||||||
}
|
|
||||||
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
@@ -1,117 +0,0 @@
|
|||||||
package humanize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Seconds-based time units
|
|
||||||
const (
|
|
||||||
Day = 24 * time.Hour
|
|
||||||
Week = 7 * Day
|
|
||||||
Month = 30 * Day
|
|
||||||
Year = 12 * Month
|
|
||||||
LongTime = 37 * Year
|
|
||||||
)
|
|
||||||
|
|
||||||
// Time formats a time into a relative string.
|
|
||||||
//
|
|
||||||
// Time(someT) -> "3 weeks ago"
|
|
||||||
func Time(then time.Time) string {
|
|
||||||
return RelTime(then, time.Now(), "ago", "from now")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A RelTimeMagnitude struct contains a relative time point at which
|
|
||||||
// the relative format of time will switch to a new format string. A
|
|
||||||
// slice of these in ascending order by their "D" field is passed to
|
|
||||||
// CustomRelTime to format durations.
|
|
||||||
//
|
|
||||||
// The Format field is a string that may contain a "%s" which will be
|
|
||||||
// replaced with the appropriate signed label (e.g. "ago" or "from
|
|
||||||
// now") and a "%d" that will be replaced by the quantity.
|
|
||||||
//
|
|
||||||
// The DivBy field is the amount of time the time difference must be
|
|
||||||
// divided by in order to display correctly.
|
|
||||||
//
|
|
||||||
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
|
||||||
// DivBy should be time.Minute so whatever the duration is will be
|
|
||||||
// expressed in minutes.
|
|
||||||
type RelTimeMagnitude struct {
|
|
||||||
D time.Duration
|
|
||||||
Format string
|
|
||||||
DivBy time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultMagnitudes = []RelTimeMagnitude{
|
|
||||||
{time.Second, "now", time.Second},
|
|
||||||
{2 * time.Second, "1 second %s", 1},
|
|
||||||
{time.Minute, "%d seconds %s", time.Second},
|
|
||||||
{2 * time.Minute, "1 minute %s", 1},
|
|
||||||
{time.Hour, "%d minutes %s", time.Minute},
|
|
||||||
{2 * time.Hour, "1 hour %s", 1},
|
|
||||||
{Day, "%d hours %s", time.Hour},
|
|
||||||
{2 * Day, "1 day %s", 1},
|
|
||||||
{Week, "%d days %s", Day},
|
|
||||||
{2 * Week, "1 week %s", 1},
|
|
||||||
{Month, "%d weeks %s", Week},
|
|
||||||
{2 * Month, "1 month %s", 1},
|
|
||||||
{Year, "%d months %s", Month},
|
|
||||||
{18 * Month, "1 year %s", 1},
|
|
||||||
{2 * Year, "2 years %s", 1},
|
|
||||||
{LongTime, "%d years %s", Year},
|
|
||||||
{math.MaxInt64, "a long while %s", 1},
|
|
||||||
}
|
|
||||||
|
|
||||||
// RelTime formats a time into a relative string.
|
|
||||||
//
|
|
||||||
// It takes two times and two labels. In addition to the generic time
|
|
||||||
// delta string (e.g. 5 minutes), the labels are used applied so that
|
|
||||||
// the label corresponding to the smaller time is applied.
|
|
||||||
//
|
|
||||||
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
|
||||||
func RelTime(a, b time.Time, albl, blbl string) string {
|
|
||||||
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomRelTime formats a time into a relative string.
|
|
||||||
//
|
|
||||||
// It takes two times two labels and a table of relative time formats.
|
|
||||||
// In addition to the generic time delta string (e.g. 5 minutes), the
|
|
||||||
// labels are used applied so that the label corresponding to the
|
|
||||||
// smaller time is applied.
|
|
||||||
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
|
||||||
lbl := albl
|
|
||||||
diff := b.Sub(a)
|
|
||||||
|
|
||||||
if a.After(b) {
|
|
||||||
lbl = blbl
|
|
||||||
diff = a.Sub(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
n := sort.Search(len(magnitudes), func(i int) bool {
|
|
||||||
return magnitudes[i].D > diff
|
|
||||||
})
|
|
||||||
|
|
||||||
if n >= len(magnitudes) {
|
|
||||||
n = len(magnitudes) - 1
|
|
||||||
}
|
|
||||||
mag := magnitudes[n]
|
|
||||||
args := []interface{}{}
|
|
||||||
escaped := false
|
|
||||||
for _, ch := range mag.Format {
|
|
||||||
if escaped {
|
|
||||||
switch ch {
|
|
||||||
case 's':
|
|
||||||
args = append(args, lbl)
|
|
||||||
case 'd':
|
|
||||||
args = append(args, diff/mag.DivBy)
|
|
||||||
}
|
|
||||||
escaped = false
|
|
||||||
} else {
|
|
||||||
escaped = ch == '%'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(mag.Format, args...)
|
|
||||||
}
|
|
||||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -33,8 +33,6 @@ github.com/bmatcuk/doublestar
|
|||||||
github.com/davecgh/go-spew/spew
|
github.com/davecgh/go-spew/spew
|
||||||
# github.com/disintegration/imaging v1.6.0
|
# github.com/disintegration/imaging v1.6.0
|
||||||
github.com/disintegration/imaging
|
github.com/disintegration/imaging
|
||||||
# github.com/dustin/go-humanize v1.0.0
|
|
||||||
github.com/dustin/go-humanize
|
|
||||||
# github.com/fsnotify/fsnotify v1.4.7
|
# github.com/fsnotify/fsnotify v1.4.7
|
||||||
github.com/fsnotify/fsnotify
|
github.com/fsnotify/fsnotify
|
||||||
# github.com/go-chi/chi v4.0.2+incompatible
|
# github.com/go-chi/chi v4.0.2+incompatible
|
||||||
|
|||||||
Reference in New Issue
Block a user