From 54f0293f0ab73f357f545f8e05d16f9b254aba5e Mon Sep 17 00:00:00 2001
From: Jonas <cez81@users.noreply.github.com>
Date: Sat, 8 Apr 2017 17:27:26 +0200
Subject: [PATCH] Mirror sync interval specified as duration string (#1407)

* Sync interval specifed as duration string

* Changed mirror interval text

* make fmt

* Add MinInterval for mirror sync

* Use duration internally

* Changed min default to 10m

* make fmt

* Incorrect default

* Removed defaults in MustDuration()

* Add Mirror interval migration

* Default values corrected

* Use transaction during migration

* Change http 500 to page with error message

* Cleanup session.commit()
---
 conf/app.ini                         |  6 ++-
 models/migrations/migrations.go      |  2 +
 models/migrations/v27.go             | 56 ++++++++++++++++++++++++++++
 models/repo.go                       |  2 +-
 models/repo_mirror.go                |  6 +--
 modules/auth/repo_form.go            |  2 +-
 modules/setting/setting.go           | 21 +++++++----
 options/locale/locale_en-US.ini      |  3 +-
 routers/repo/setting.go              | 11 ++++--
 templates/repo/settings/options.tmpl |  2 +-
 10 files changed, 90 insertions(+), 21 deletions(-)
 create mode 100644 models/migrations/v27.go

diff --git a/conf/app.ini b/conf/app.ini
index 47dcffc37e..b1eb953cdf 100644
--- a/conf/app.ini
+++ b/conf/app.ini
@@ -461,8 +461,10 @@ PULL = 300
 GC = 60
 
 [mirror]
-; Default interval in hours between each check
-DEFAULT_INTERVAL = 8
+; Default interval as a duration between each check
+DEFAULT_INTERVAL = 8h
+; Min interval as a duration must be > 1m
+MIN_INTERVAL = 10m
 
 [api]
 ; Max number of items will response in a page
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 07a56e879b..fee0fef79c 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -102,6 +102,8 @@ var migrations = []Migration{
 	NewMigration("add show field in user openid table", addUserOpenIDShow),
 	// v26 -> v27
 	NewMigration("generate and migrate repo and wiki Git hooks", generateAndMigrateGitHookChains),
+	// v27 -> v28
+	NewMigration("change mirror interval from hours to time.Duration", convertIntervalToDuration),
 }
 
 // Migrate database to current version
diff --git a/models/migrations/v27.go b/models/migrations/v27.go
new file mode 100644
index 0000000000..0ac9553725
--- /dev/null
+++ b/models/migrations/v27.go
@@ -0,0 +1,56 @@
+// Copyright 2017 Gitea. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/go-xorm/xorm"
+)
+
+func convertIntervalToDuration(x *xorm.Engine) (err error) {
+	type Repository struct {
+		ID      int64
+		OwnerID int64
+		Name    string
+	}
+	type Mirror struct {
+		ID          int64       `xorm:"pk autoincr"`
+		RepoID      int64       `xorm:"INDEX"`
+		Repo        *Repository `xorm:"-"`
+		Interval    time.Duration
+		EnablePrune bool `xorm:"NOT NULL DEFAULT true"`
+
+		Updated        time.Time `xorm:"-"`
+		UpdatedUnix    int64     `xorm:"INDEX"`
+		NextUpdate     time.Time `xorm:"-"`
+		NextUpdateUnix int64     `xorm:"INDEX"`
+
+		address string `xorm:"-"`
+	}
+
+	sess := x.NewSession()
+	defer sess.Close()
+
+	if err := sess.Begin(); err != nil {
+		return err
+	}
+
+	var mirrors []Mirror
+	err = sess.Table("mirror").Select("*").Find(&mirrors)
+	if err != nil {
+		return fmt.Errorf("Query repositories: %v", err)
+	}
+	for _, mirror := range mirrors {
+		mirror.Interval = mirror.Interval * time.Hour
+		_, err := sess.Id(mirror.ID).Cols("interval").Update(mirror)
+		if err != nil {
+			return fmt.Errorf("update mirror interval failed: %v", err)
+		}
+	}
+
+	return sess.Commit()
+}
diff --git a/models/repo.go b/models/repo.go
index 8548accb5d..c654356916 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -815,7 +815,7 @@ func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) {
 			RepoID:      repo.ID,
 			Interval:    setting.Mirror.DefaultInterval,
 			EnablePrune: true,
-			NextUpdate:  time.Now().Add(time.Duration(setting.Mirror.DefaultInterval) * time.Hour),
+			NextUpdate:  time.Now().Add(setting.Mirror.DefaultInterval),
 		}); err != nil {
 			return repo, fmt.Errorf("InsertOne: %v", err)
 		}
diff --git a/models/repo_mirror.go b/models/repo_mirror.go
index d8d4bc0bec..67ae449bde 100644
--- a/models/repo_mirror.go
+++ b/models/repo_mirror.go
@@ -27,8 +27,8 @@ type Mirror struct {
 	ID          int64       `xorm:"pk autoincr"`
 	RepoID      int64       `xorm:"INDEX"`
 	Repo        *Repository `xorm:"-"`
-	Interval    int         // Hour.
-	EnablePrune bool        `xorm:"NOT NULL DEFAULT true"`
+	Interval    time.Duration
+	EnablePrune bool `xorm:"NOT NULL DEFAULT true"`
 
 	Updated        time.Time `xorm:"-"`
 	UpdatedUnix    int64     `xorm:"INDEX"`
@@ -68,7 +68,7 @@ func (m *Mirror) AfterSet(colName string, _ xorm.Cell) {
 
 // ScheduleNextUpdate calculates and sets next update time.
 func (m *Mirror) ScheduleNextUpdate() {
-	m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour)
+	m.NextUpdate = time.Now().Add(m.Interval)
 }
 
 func (m *Mirror) readAddress() {
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index 7fdb5e67e2..dd3fbff7bf 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -88,7 +88,7 @@ type RepoSettingForm struct {
 	RepoName      string `binding:"Required;AlphaDashDot;MaxSize(100)"`
 	Description   string `binding:"MaxSize(255)"`
 	Website       string `binding:"Url;MaxSize(255)"`
-	Interval      int
+	Interval      string
 	MirrorAddress string
 	Private       bool
 	EnablePrune   bool
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 5a0666ade2..c2e08b0c14 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -417,10 +417,9 @@ var (
 	}
 
 	// Mirror settings
-	Mirror = struct {
-		DefaultInterval int
-	}{
-		DefaultInterval: 8,
+	Mirror struct {
+		DefaultInterval time.Duration
+		MinInterval     time.Duration
 	}
 
 	// API settings
@@ -886,14 +885,20 @@ please consider changing to GITEA_CUSTOM`)
 		log.Fatal(4, "Failed to map Cron settings: %v", err)
 	} else if err = Cfg.Section("git").MapTo(&Git); err != nil {
 		log.Fatal(4, "Failed to map Git settings: %v", err)
-	} else if err = Cfg.Section("mirror").MapTo(&Mirror); err != nil {
-		log.Fatal(4, "Failed to map Mirror settings: %v", err)
 	} else if err = Cfg.Section("api").MapTo(&API); err != nil {
 		log.Fatal(4, "Failed to map API settings: %v", err)
 	}
 
-	if Mirror.DefaultInterval <= 0 {
-		Mirror.DefaultInterval = 24
+	sec = Cfg.Section("mirror")
+	Mirror.MinInterval = sec.Key("MIN_INTERVAL").MustDuration(10 * time.Minute)
+	Mirror.DefaultInterval = sec.Key("DEFAULT_INTERVAL").MustDuration(8 * time.Hour)
+	if Mirror.MinInterval.Minutes() < 1 {
+		log.Warn("Mirror.MinInterval is too low")
+		Mirror.MinInterval = 1 * time.Minute
+	}
+	if Mirror.DefaultInterval < Mirror.MinInterval {
+		log.Warn("Mirror.DefaultInterval is less than Mirror.MinInterval")
+		Mirror.DefaultInterval = time.Hour * 8
 	}
 
 	Langs = Cfg.Section("i18n").Key("LANGS").Strings(",")
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index c843a4b398..cfb4da30ae 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -440,7 +440,8 @@ create_repo = Create Repository
 default_branch = Default Branch
 mirror_prune = Prune
 mirror_prune_desc = Remove any remote-tracking references that no longer exist on the remote
-mirror_interval = Mirror Interval (hour)
+mirror_interval = Mirror interval (valid time units are "h", "m", "s")
+mirror_interval_invalid = Mirror interval is not valid
 mirror_address = Mirror Address
 mirror_address_desc = Please include necessary user credentials in the address.
 mirror_last_synced = Last Synced
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index fb6d68ad9b..ed7254fe1b 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -111,12 +111,15 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
 			return
 		}
 
-		if form.Interval > 0 {
+		interval, err := time.ParseDuration(form.Interval)
+		if err != nil || interval < setting.Mirror.MinInterval {
+			ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+		} else {
 			ctx.Repo.Mirror.EnablePrune = form.EnablePrune
-			ctx.Repo.Mirror.Interval = form.Interval
-			ctx.Repo.Mirror.NextUpdate = time.Now().Add(time.Duration(form.Interval) * time.Hour)
+			ctx.Repo.Mirror.Interval = interval
+			ctx.Repo.Mirror.NextUpdate = time.Now().Add(interval)
 			if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
-				ctx.Handle(500, "UpdateMirror", err)
+				ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
 				return
 			}
 		}
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index dbe7d81f84..8ad09d4306 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -56,7 +56,7 @@
 					</div>
 					<div class="inline field {{if .Err_Interval}}error{{end}}">
 						<label for="interval">{{.i18n.Tr "repo.mirror_interval"}}</label>
-						<input id="interval" name="interval" type="number" value="{{.MirrorInterval}}">
+						<input id="interval" name="interval" value="{{.MirrorInterval}}">
 					</div>
 					<div class="field">
 						<label for="mirror_address">{{.i18n.Tr "repo.mirror_address"}}</label>