Пишем “тоссер” на Go

zaz600
Golang Notes
Published in
16 min readDec 22, 2015

--

У меня на работе периодически возникает необходимость перемещать файлы, выгруженные из одной программы или комплекса, во входящие каталоги другой системы либо на сетевой диск, чтобы их забрал пользователь. Обычно в простейшем случае для этого используются пакетные файлы или shell-скрипты в комбинации с cron-ом/планировщиком.

Попробуем для этих целей написать программу на Go.

Какие моменты мы затронем в этой статье.

  1. Работа с YAML, JSON.
  2. Каналы, мьютексы.
  3. Горутины.
  4. Создание веб-сервера.
  5. Копирование и перемещение файлов.

В заголовке написано “тоссер”, но наша программа будет обладать лишь способностью перекладывать файлы, что является одной из функций тоссеров. Подробнее про настоящие эхопроцессоры можно прочитать тут.

Напомню, что я не профессиональный программист на Go, а только учусь. Если какие-то реализации кода, увиденные в статье вам покажутся не go-way, жду пулл-реквестов, респонсов и т.п. :)

Сформируем требования к программе.

  1. Хранение настроек в файле.
  2. Перемещение файлов между локальными каталогами и/или сетевыми дисками.
  3. Одновременная обработка более одной пары каталогов источник-назначение, то есть работа в несколько потоков.
  4. Наличие в настройках вариантов выбора действия, если файл в конечной папке уже существует. Например, перезапись и пропуск.
  5. Возможность задать для одной сканируемой папки несколько правил обработки (какие файлы искать и в куда их перемещать).
  6. Возможность задавать списки исключений для файлов.
  7. Отображение статистики работы в веб-браузере.

Для простоты отбросим необходимость обрабатывать каталоги рекурсивно.

Примеры кода можно скачать на github.

git clone https://github.com/zaz600/gotosser
git checkout tags/ch01

После клонирования репозитория надо переименовать файл gotosser.yaml.example в gotosser.yaml и настроить в нем каталог-источник и каталог-назначения. Ниже будет представлено содержимое этого файла.

Первая версия

Напишем основу для нашей программы. Первая версия будет уметь считывать конфигурационный файл определенного формата, выдавать список каталогов, которые подлежат сканированию, ожидать закрытия программы. Никаких других опций пока делать не будем.

Для начала создадим конфигурационный файл в формате YAML.
Хотя он и не поддерживается “из коробки”, но он более удобен для чтения и правки человеком, чем JSON.

Содержимое файла gotosser.yaml, который будем использовать (скачайте его себе или создайте такой же):

Пример файла конфигурации с комментариями можно посмотреть здесь.

В настройках мы задаем каталог-источник, который будем периодически сканировать, правила для поиска файлов и их перемещения в каталог-приёмник, паузу в секундах между повторным сканированием каталогов и действие при существовании файла с таким именем в каталоге-приёмнике. В файле указаны и другие общие параметры, которые нам сейчас не важны.

Итак, создаем файл gotosser.go.

func main() {
//загружаем конфиг
cfg, err := reloadConfig(configFileName)
if err != nil {
if err != errNotModified {
log.Fatalf("Не удалось загрузить %s: %s", configFileName, err)
}
}
//log.Printf("%#v", cfg)
//запускаем цикл сканирования каталогов
go scanLoop(cfg)
//ожидаем завершение программы по Ctrl-C
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
signal.Notify(sigChan, syscall.SIGTERM)
for {
select {
case <-sigChan:
log.Println("CTRL-C: Завершаю работу.")
return
}
}
}

После запуска программа считывает конфигурационный файл, запускает отдельную горутину, в которой будет происходить сканирование каталогов, а затем ожидает нажатия Ctrl-C. Если не сделать бесконечный цикл внутри main или цикл ожидания прерывания, то main просто завершится, не обращая внимания на запущенные в фоне горутины.

Процедура scanLoop пока просто печатает список каталогов, которые необходимо сканировать.

func scanLoop(cfg *Config) {
//периодически просматриваем конфиг и помечаем каталоги из него для сканирования файлов внутри
for {
for _, scangroup := range cfg.ScanGroups {
if scangroup.Enabled != true {
continue
}
for _, srcDir := range scangroup.SrcDirs {
log.Println("Сканируем каталог", srcDir)
}
}
time.Sleep(time.Duration(cfg.RescanInterval) * time.Second)
}
}

Для чтения настроек будем использовать пакет “gopkg.in/yaml.v2”.

Создаем файл config.go в который поместим логику работы с конфигурационным файлом.

Сначала мы описываем структуру Config, которая будет заполняться в результате выполнения функции yaml.Unmarshal. Обратите внимание в структуре мы задали названия полей с большой буквы, а рядом оставили “подсказку” для yaml.Unmarshal, как на самом деле называются поля внутри файла.

Выкачиваем необходимые пакеты и компилируем программу.

git checkout tags/ch01
go get
go build

Запускаем и видим примерно такой результат:

2015/12/19 19:46:11 Сканируем каталог src1\
2015/12/19 19:46:16 Сканируем каталог src1\
2015/12/19 19:46:21 Сканируем каталог src1\
2015/12/19 19:46:23 CTRL-C: Завершаю работу.

Исходник к первой части можно посмотреть тут.

Вторая версия

Теперь добавим процедуру сканирования папки.

func processScanGroup(scangroup ScanGroup) {
for _, srcDir := range scangroup.SrcDirs {
fullSrcDir := srcDir
abspath, err := filepath.Abs(fullSrcDir)
if err != nil {
log.Println("Ошибка вычисления абсолютного пути", srcDir, err)
continue
}
fullSrcDir = abspath
log.Println("Сканируем каталог", fullSrcDir)
//читаем содержимое каталога
items, err := ioutil.ReadDir(fullSrcDir)
if err != nil {
log.Println(err)
log.Printf("Обработка каталога завершена %s", fullSrcDir)
continue
}
//обрабатываем файлы
processItems(items, fullSrcDir)
}
}

Процедура читает содержимое каталога и передает найденные файлы в процедуру processItems, которая будет их обрабатывать. Пока она их просто выводит в консоль.

func processItems(items []os.FileInfo, fullSrcDir string) {
for _, item := range items {
// обрабатываем только файлы. Не каталоги, символические ссылки и т.п.
if !item.Mode().IsRegular() {
continue
}
srcFile := item.Name()
fullSrcFilePath := filepath.Join(fullSrcDir, srcFile)
log.Println(fullSrcFilePath)
}
}

Добавляем запуск processScanGroup в scanLoop.

// scanLoop просматривает конфиг и для каждого каталога-источника
// запускает горутину processScanGroup
func scanLoop(cfg *Config) {
for {
for _, scangroup := range cfg.ScanGroups {
if scangroup.Enabled != true {
continue
}
go processScanGroup(scangroup)
}
time.Sleep(time.Duration(cfg.RescanInterval) * time.Second)
}
}

Процедуру processScanGroup мы запускаем в отдельном потоке.
Теоретически в конфигурационном файле можно указать 1000 каталогов-источников, тогда scanLoop попытается запустить 1000 копий processScanGroup.

Нам необходимо ограничить максимальное число запускаемых процедур processScanGroup, работающих одновременно.
Это можно сделать двумя способами.

Во-первых, мы можем создать канал, для общения между процедурами scanLoop и processScanGroup. И перед запуском цикла в scanLoop запустить нужное конечное число процедур processScanGroup, которые будут считывать данные из этого канала.

func scanLoop(cfg *Config) {
ch := make(chan ScanGroup)
for i := 0; i < 5; i++ {
go processScanGroup(ch)
}
for {
for _, scangroup := range cfg.ScanGroups {
if scangroup.Enabled != true {
continue
}
ch <- scangroup
}
...
func processScanGroup(ch chan ScanGroup) {
for scangroup := range ch {
//обрабатываем
}
}

Во-вторых, можно создать буферизированный канал типа struct{}определенного размера. И перед запуском go processScanGroup отправлять элемент в этот канал. Если окажется так, что у нас уже запущено число горутин processScanGroup равное объему такого канала, то очередная операция отправки в него элемента заблокируется.

var (
//контролируем число потоков processScanGroup
tokens = make(chan struct{}, 8)
)
...
func scanLoop(cfg *Config) {
for {
for _, scangroup := range cfg.ScanGroups {
if scangroup.Enabled != true {
continue
}
//захватываем токен.
//в этом месте будет пауза, если окажется,
//что число запущенных горутин processScanDir больше,
//чем вместимость tokens
tokens <- struct{}{}
go processScanGroup(scangroup)
}
time.Sleep(time.Duration(cfg.RescanInterval) * time.Second)
}
}
func processScanGroup(scangroup ScanGroup) {
//освобождаем токен после завершения процедуры
defer func() { <-tokens }()|
for _, srcDir := range scangroup.SrcDirs {

Теперь сделаем так, чтобы в конфигурационном файле в пути к каталогу-источнику можно было задавать дату/время. Например, c:\test\%Y%m%d. Тогда тоссер сможет динамически менять каталог-источник в зависимости от даты. Воспользуемся пакетом github.com/hhkbp2/go-strftime, который позволяет работать с датой в подобном формате.

import (
...
“github.com/hhkbp2/go-strftime”
)

func processScanGroup(scangroup ScanGroup) {
//освобождаем токен после завершения процедуры
defer func() { <-tokens }()
for _, srcDir := range scangroup.SrcDirs {
//разворачиваем маску времени (%Y%m%d и т.п.), если есть в пути
fullSrcDir := strftime.Format(srcDir, time.Now())
abspath, err := filepath.Abs(fullSrcDir)
...

Полный текст программы можно посмотреть тут.
Выкачиваем необходимые пакеты и компилируем программу.

git checkout tags/ch02
go get
go build

Закинем несколько файлов в каталог src1 и запустим программу.

2015/12/19 23:50:03 Сканируем каталог C:\go_proj\src\medium\gotosser\ch02\src1\
2015/12/19 23:50:03 C:\go_proj\src\medium\gotosser\ch02\src1\1111111111111111
2015/12/19 23:50:03 C:\go_proj\src\medium\gotosser\ch02\src1\config.go
2015/12/19 23:50:03 C:\go_proj\src\medium\gotosser\ch02\src1\gotosser.exe
2015/12/19 23:50:08 Сканируем каталог C:\go_proj\src\medium\gotosser\ch02\src1\
2015/12/19 23:50:08 C:\go_proj\src\medium\gotosser\ch02\src1\1111111111111111
2015/12/19 23:50:08 C:\go_proj\src\medium\gotosser\ch02\src1\config.go
2015/12/19 23:50:08 C:\go_proj\src\medium\gotosser\ch02\src1\gotosser.exe
2015/12/19 23:50:09 CTRL-C: Завершаю работу.

Третья версия

В этой версии сделаем так, чтобы программа обрабатывала не все файлы подряд, а только те, что попадают под маски заданные в правилах в конфигурационном файле.

Создаём процедуру processItem, которая будет общаться с процедурой processItems через канал processingchan.

var (
...
processingchan = make(chan processingItem, 100)
)
//копирует или перемещает конкретный файл
//в зависимости от заданных правил
func processItem() {
for item := range processingchan {
//Проверяем правила
for _, k := range item.scangroup.getRuleKeys() {
rule := item.scangroup.Rules[k]
//Проверяем маски
if matched, _ := rule.match(item.srcFile); !matched {
continue
}
//файл подошел под маски правила
fullDstFilePath, err := getAbsPath(rule.DstDir, item.srcFile)
if err != nil {
log.Println(“Ошибка вычисления абсолютного пути”, err)
continue
}
switch rule.Mode {
case “move”:
moveFile(item.fullSrcFilePath, fullDstFilePath)
//тут надо обработать возможные ошибки
case “copy”:
copyFile(item.fullSrcFilePath, fullDstFilePath)
//тут надо обработать возможные ошибки
default:
log.Println(“Неизвестный режим”, rule.Mode)
}
}
}
}

Процедура processItem читает из канала элементы и проверяет их на совпадение с масками правил. Чтобы правила обрабатывались не в случайном порядке (так как словарь — это неупорядоченная последовательность), а в соответствии с их номерами, мы делаем выборку правил по отсортированным ключам типа int.

//config.go
func (sd ScanGroup) getRuleKeys() []int {
var keys []int
for k := range sd.Rules {
keys = append(keys, k)
}
sort.Ints(keys)
return keys
}
//gottossser.go
for _, k := range item.scangroup.getRuleKeys() {
rule := item.scangroup.Rules[k]

Проверка файла на соответствие определенной маске выполняется с помощью функции Match из стандартного пакета filepath.

//config.go
//проверяет подходит ли файл под маски данного правила
//возвращает список масок
func (r CopyRule) match(srcFile string) (bool, []string) {
var masks []string
for _, mask := range r.Masks {
matched, err := filepath.Match(strings.ToLower(mask), strings.ToLower(srcFile))
if err != nil {
log.Printf("Ошибка проверки MASK (%s). %s", mask, err)
continue
}
if matched {
masks = append(masks, mask)
}
}
if len(masks) == 0 {
return false, masks
}
return true, masks
}

Так как в конфигурационном файле для каталога-назначения также полезно иметь возможность задать маску даты(c:\out\%Y%m%d), мы вынесли этот блок в отдельную функцию.

func getAbsPath(dir, file string) (string, error) {
filePath := filepath.Join(strftime.Format(dir, time.Now()), file)
abspath, err := filepath.Abs(filePath)
if err != nil {
return “”, err
}
return abspath, nil
}

Функции для перемещения и копирования файлов:

//перемещаем файл
func moveFile(src, dst string) error {
err := os.Rename(src, dst)
if err != nil {
return err
}
return nil
}
//копируем файл
func copyFile(src string, dst string) (err error) {
sourcefile, err := os.Open(src)
if err != nil {
return err
}
defer sourcefile.Close()
destfile, err := os.Create(dst)
if err != nil {
return err
}
//копируем содержимое и проверяем коды ошибок
_, err = io.Copy(destfile, sourcefile)
if closeErr := destfile.Close(); err == nil {
//если ошибки в io.Copy нет, то берем ошибку от destfile.Close(), если она была
err = closeErr
}
if err != nil {
return err
}
sourceinfo, err := os.Stat(src)
if err == nil {
err = os.Chmod(dst, sourceinfo.Mode())
}
return err
}

Вносим изменения в processItems и processScangroup.

func processScanGroup(scangroup ScanGroup) {
...
//обрабатываем файлы
processItems(items, fullSrcDir, scangroup)

func processItems(items []os.FileInfo, fullSrcDir string, scangroup ScanGroup) {
...
processingchan <- processingItem{srcFile, fullSrcFilePath, scangroup, item.Size()}
...

Полный текст программы можно найти в тут.

git checkout tags/ch03
go build

Компилируем, кладем в папку src1 несколько файлов, создаем папку dst1_1 (так как наша программа пока не умеет создавать недостающие каталоги) и запускаем программу.
В результате файлы должны быть перемещены из src1 в dst1_1.
Кстати, программа пока ничего не знает, что ей делать, если файл в конечном каталоге уже существует.

Четвертая часть

В принципе наша программа уже вполне рабочая за исключением нескольких моментов.

  1. Если копирование/перемещение файла будет выполняться долго, то процедура processItems попытается повторно закинуть файл в канал копирования. И если свободная горутина processItem захватит такой файл, то очень вероятно произойдет ошибка копирования, так как он будет занят другой горутиной, которая начала первой его обрабатывать. Нам необходимо сделать так, чтобы файл не ставился в обработку дважды.
  2. Необходимо сделать так, чтобы отсутствующие каталоги создавались автоматически для каталога-назначения и если указано в конфигурационном файле (create_src) для каталога-источника.
  3. Сейчас программа ничего не знает, что ей делать, если файл в целевом каталоге уже существует.
  4. Надо добавить обработку масок исключений, заданных в настройках.

Итак, сосредоточимся на данных функциях.

Исключение обработки одного файла дважды

Для того чтобы файлы не попадали в обработку дважды, сделаем “кэш” обрабатываемых файлов и перед отправкой файла в обработку, проверим нет ли его уже в кэше.

Создаем файл processing_cache.go

//processing_cache.go
package main
import (
"sync"
)
//processingCache - используется для отслеживания уже обрабатываемых файлов и каталогов
type processingCache struct {
sync.RWMutex
cache map[string]int
}
func (p *processingCache) add(fullSrcPath string) {
p.Lock()
p.cache[fullSrcPath] = 1
p.Unlock()
}
func (p *processingCache) del(fullSrcPath string) {
p.Lock()
delete(p.cache, fullSrcPath)
p.Unlock()
}
func (p *processingCache) check(fullSrcPath string) bool {
p.Lock()
_, ok := p.cache[fullSrcPath]
p.Unlock()
return ok
}
//NewProcessingCache - создает и возвращает ссылку на processingCache
func NewProcessingCache() *processingCache {
p := new(processingCache)
p.cache = make(map[string]int)
return p
}

Чтобы две горутины не пытались вставить один и тот же файл одновременно в кеш мы используем RWMutex.

Добавляем создание кэша в объявление.

var (
...
processing = NewProcessingCache()
)

И проверки на наличие файла в кэше.

func processItems(items []os.FileInfo, fullSrcDir string, scangroup ScanGroup) {
for _, item := range items {
// обрабатываем только файлы. Не каталоги, символические ссылки и т.п.
if !item.Mode().IsRegular() {
continue
}
srcFile := item.Name()
fullSrcFilePath := filepath.Join(fullSrcDir, srcFile)
if processing.check(fullSrcFilePath) == true {
log.Println("файл уже обрабатывается", fullSrcFilePath)

}

//тут надо проверить маски исключения

//добавляем файл в кэш
processing.add(fullSrcFilePath)

processingchan <- processingItem{srcFile, fullSrcFilePath, scangroup, item.Size()}
log.Println(fullSrcFilePath)
}
}

func processItem() {
for item := range processingchan {
//Проверяем правила
for _, k := range item.scangroup.getRuleKeys() {
...
default:
log.Println(“Неизвестный режим”, rule.Mode)
}
//тут надо проверить, если файл перемещён, то другие правила проверять нет смысла
}
//после обработки всеми правилами удаляем файл из кэша
processing.del(item.fullSrcFilePath)
}
}

Вставим паузу в процедуру перемещения, чтобы проверить работу кэша.


//перемещаем файл
func moveFile(src, dst string) error {
time.Sleep(1*time.Minute)
err := os.Rename(src, dst)

Компилируем, кидаем файл во входящий каталог и запускаем программу.

Также сделаем так, чтобы один и тот же каталог не ставился на сканирование дважды. Это необходимо, чтобы исключить ситуацию, когда одна горутина сканирования еще обрабатывает список файлов (например, их много или чтение списка замедлено), а вторая получает тот же самый список и запускает обработку файлов, до которых первая горутина еще не дошла. Также это поможет исключить ненужные операции (получение списка файлов, его обработка, проверка на кэш и т.п.).

func processScanGroup(scangroup ScanGroup) {
...
//обрабатываем файлы
processing.add(fullSrcDir)
processItems(items, fullSrcDir, scangroup)
processing.del(fullSrcDir)

Вставляем паузу для теста в processItems.

func processItems(items []os.FileInfo, fullSrcDir string, scangroup ScanGroup) {
time.Sleep(1*time.Minute)

Компилируем и проверяем.

Убираем из кода тестовые паузы.

Создание каталогов

Для создания каталогов воспользуемся функцией os.MkdirAll

func processItem() {
...
//файл подошел под маски правила
fullDstFilePath, err := getAbsPath(rule.DstDir, item.srcFile)
...
//создаем каталоги
fullDstFileDir := filepath.Dir(fullDstFilePath)
if err := os.MkdirAll(fullDstFileDir, os.ModeDir); err != nil {
log.Println(“Ошибка создания каталога”, fullDstFileDir, err)
continue
}
func processScanGroup(scangroup ScanGroup) {
...
//создаем каталоги, если необходимо
if scangroup.СreateSrc {
if err := os.MkdirAll(fullSrcDir, os.ModeDir); err != nil {
log.Println(“Ошибка создания каталога”, fullSrcDir, err)
continue
}

Компилируем. Удаляем каталоги. Запускаем. Каталог-источник должен создаться автоматически. Если в него после этого кинуть файл, то он должен переместиться в каталог-назначения, который тоже создастся сам.

Проверка на существование файла

Проверим, что файл существует функцией os.Stat, а затем удалим его, если задано ifexists: replace.

func processItem() {
...
//если файл уже существует
if _, err := os.Stat(fullDstFilePath); err == nil {
switch rule.IfExists {
case "replace":
log.Printf("Файл существует. %s ifexists=%s. Удаляем файл в конечном каталоге", fullDstFilePath, rule.IfExists)
if err := os.Remove(fullDstFilePath); err != nil {
log.Println(err)
continue
}
case "skip":
log.Printf("Файл существует. %s ifexists=%s. Пропускаем файл", fullDstFilePath, rule.IfExists)
continue
default:
log.Printf("Файл существует. %s Неизвестное значение ifexists=%s. Пропускаем файл", fullDstFilePath, rule.IfExists)
continue
}
}

Список исключений

Всего у нас предусмотрено три списка исключений.

  1. Глобальный - действует на все файлы.
  2. Локальный для группы - действует только на файлы внутри папок группы
  3. Локальный для правила - действует только на файлы, которые попали под маски определенного правила.
//config.go/*проверки на исключение*/
//проверка глобального списка исключений
func (c Config) matchExclude(srcFile string) bool {
for _, mask := range c.GlobalExcludeMasks {
matched, err := filepath.Match(strings.ToLower(mask), strings.ToLower(srcFile))
if err != nil {
log.Printf(“Ошибка проверки MASK (%s). %s”, mask, err)
continue
}
if matched {
return true
}
}
return false
}
//проверка списка исключений группы
func (sd ScanGroup) matchExclude(srcFile string) bool {
for _, mask := range sd.ExcludeMasks {
matched, err := filepath.Match(strings.ToLower(mask), strings.ToLower(srcFile))
if err != nil {
log.Printf(“Ошибка проверки MASK (%s). %s”, mask, err)
continue
}
if matched {
return true
}
}
return false
}
//проверка списка исключений правила
func (r CopyRule) matchExclude(srcFile string) bool {
for _, mask := range r.ExcludeMasks {
matched, err := filepath.Match(strings.ToLower(mask), strings.ToLower(srcFile))
if err != nil {
log.Printf(“Ошибка проверки MASK (%s). %s”, mask, err)
continue
}
if matched {
return true
}
}
return false
}

Проверки осуществляются с помощью уже знакомой функции filepath.Match.

Делаем функцию для проверки исключений.

//проверка на исключения из правил
func needExclude(file string, scangroup ScanGroup, rule CopyRule) bool {
//пропуск файлов по маскам, заданным в настройках группы
if cfg.matchExclude(file) {
return true
}
//пропуск файлов по маскам, заданным в настройках группы
if scangroup.matchExclude(file) {
return true
}
//пропуск файлов по маскам, заданным в настройках правила
if rule.matchExclude(file) {
return true
}
return false
}
func processItem() {
...
if matched, _ := rule.match(item.srcFile); !matched {
continue
}
//проверяем исключения
if needExclude(item.srcFile, item.scangroup, rule) {
continue
}
...

Правда, для проверки глобального списка исключений, нам потребовался доступ к переменной cfg и мы её сделали глобальной.

var (
cfg *Config
)
func main() {
//загружаем конфиг
var err error
cfg, err = reloadConfig(configFileName)

В присваивании reloadConfig мы убрали двоеточие, чтобы не создавалась локальная переменная, а выражение присваивалось глобальной.

Компилируем. Проверяем.

git checkout tags/ch04
go get
go build

Исходник можно посмотреть тут.

Пятая часть

В этой части мы реализуем подсчет статистики по переданным файлам для каждого каталога и отображение этой статистики в веб-браузере.

Разделим эту часть на два блока.

Подсчет статистики

Статистика будет собираться по каждому каталогу-источнику отдельно. Нас интересует количество переданных файлов из каталога-источника и их объем. Если файл передавался в два разных каталога-назначения, то учитываем такой файл один раз.

Для того чтобы подсчитывать статистику нам понадобиться немного модифицировать имеющийся код. В частности, нам надо добавить проверку на то, успешно ли скопровался/переместился файл. Если да, тогда сохраняем по этому файлу статистику.

func processItem() {
for item := range processingchan {
//Проверяем правила
fileProcessed := false
for _, k := range item.scangroup.getRuleKeys() {
rule := item.scangroup.Rules[k]
...
//Обработка файла
fileMoved := false
switch rule.Mode {
...
//если файл перемещён, то другие правила проверять нет смысла
if fileMoved {
break
}

}
if fileProcessed {
//файл обработан сохраняем статистику
savestatchan <- item
}
//после обработки всеми правилами удаляем файл из кэша
processing.del(item.fullSrcFilePath)
}
}

Выше мы добавили две переменные для проверки успешности обработки файла и проверки на перемещение, чтобы зря не прогонять уже перемещённый файл по оставшимся правилам, которые ничего не смогут с ним сделать. Также мы добавили канал, куда будем отправлять информацию по успешно обработанному файлу.

const (
configFileName = “gotosser.yaml”
statfile = “tmp/stat.json”
)
var (
...
tosserstat = NewTosserStat(statfile)
savestatchan chan processingItem

)

Создаем канал, про который написано выше, и переменную tosserstat, которая имеет тип *TosserStat.

//tosser.go
type TosserStat struct {
Dates map[string]map[string]*dirStatInfo
ConfigName string
}
type dirStatInfo struct {
//количество файлов
Count int64
//дата последней передачи файла
LastProcessingDate int64
//общий размер файлов
TotalSize int64
}

У структуры TosserStat есть методы load, save, update и т. п. Посмотреть полный текст stat.go можно тут. Вся статистика сохраняется в формате JSON в папке tmp/stat.json. Папка задаётся в константе statfile. Сохранение происходит каждые 10 секунд, обновление - как только что-нибудь будет передано в канал savestatchan.

Добавляем запуск цикла обновления статистики.

func main() {

//запускаем цикл сканирования каталогов
go scanLoop(cfg)
//запускаем горутину, которая сохраняет статистику в файл
savestatchan = SaveStatLoop(tosserstat)

Компилируем, запускаем. После передачи файлов видим, что в папке tmp создался файл stat.go.

Статистика в веб-браузере

Более подробную статью про веб-доступ можно прочитать тут:

Здесь же, мы не будем пользоваться шаблонами, а код веб-страницы “зашьём” прямо в исходник нашей программы.

Ниже код файла http_server.go, который нам обеспечит возможность просматривать статистику в браузере.

Адрес и порт сервера берётся из конфигурационного файла. Для создания веб-сервера мы пользуемся стандартным пакетом net/http. Данные, которые отображаются в браузере, предварительно сортируются по имени папки-источника.

Компилируем, запускаем, проверяем.

git checkout tags/ch05
go get
go build

Исходник можно посмотреть тут.

Шестая часть. Заключительная

Что еще можно сделать:

  1. Реализовать в программе разные уровни логирования.
  2. Добавить логирование в файл с ротацией логов.
  3. Вести историю переданных файлов.
  4. На веб-страницу выводить сообщения об ошибках, чтобы не приходилось постоянно заглядывать в логи.

Совсем кратко опишу, как это все сделать.

Для реализации пунктов 1–3 будем использовать пакет gopkg.in/natefinch/lumberjack.v2

Файл logging.go

func initLogger(cfg *Config) error {
syncOnce.Do(func() {
LumberjackLogger = &lumberjack.Logger{
Filename: “logs/gotosser.log”,
MaxSize: 30, // megabytes
MaxBackups: 5,
MaxAge: 30, //days
LocalTime: true,
}
})
if err := LumberjackLogger.Rotate(); err != nil {
return fmt.Errorf(“Failed to open log file: %s”, err)
}
if strings.ToUpper(cfg.LogLevel) == “DEBUG” {
multi := io.MultiWriter(LumberjackLogger, os.Stdout)
Debug = log.New(multi, “DEBUG: “, log.Ldate|log.Ltime)
Info = log.New(multi, “INFO: “, log.Ldate|log.Ltime)
} else if strings.ToUpper(cfg.LogLevel) == “INFO” {
multi := io.MultiWriter(LumberjackLogger, os.Stdout)
Debug = log.New(ioutil.Discard, “DEBUG: “, log.Ldate|log.Ltime)
Info = log.New(multi, “INFO: “, log.Ldate|log.Ltime)
} else if strings.ToUpper(cfg.LogLevel) == “ERROR” {
Debug = log.New(ioutil.Discard, “DEBUG: “, log.Ldate|log.Ltime)
Info = log.New(ioutil.Discard, “INFO: “, log.Ldate|log.Ltime)
}
Info.Println(“Уровень логирования”, cfg.LogLevel)
multi := io.MultiWriter(LumberjackLogger, os.Stderr)
Error = log.New(multi, “ERROR: “, log.Ldate|log.Ltime|log.Lshortfile)
file, err := os.OpenFile(“logs/files.log”, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return fmt.Errorf(“Failed to open files.log file: %s”, err)
}
FileLog = log.New(file, “”, log.Ldate|log.Ltime)
return nil
}

В начале функции initLogger однократно создаем экземпляр LumberjackLogger с нужными нам параметрами. Однократность необходима для того, чтобы переинициализировать логер после перезагрузки конфигурационного файла тоссера.

Затем инициализируем логеры: Debug, Info, Error.

При уровне логирования Debug мы будем видеть все сообщения в логе.
При уровне Info все, кроме Debug.
При уровне Error в лог будут попадать только сообщения об ошибках.

Чтобы сообщения писались как в файл, так и на экран, мы используем io.MultiWriter. Ему передаются объекты с интерфейсом Reader, в которые будет производиться передача сообщений лога.

Если нам не надо, чтобы сообщения отправленные в логер Debug отображались на экране и в файле, когда у нас задан режим INFO или ERROR в настройках, то мы в инициализацию Debug добавляем в качестве приемника ioutil.Discard.

Пользоваться логерами можно так:

Debug.Println(“Сканируем каталог”, fullSrcDir)
Info.Printf("Файл существует. %s ifexists=%s. Удаляем файл в конечном каталоге", fullDstFilePath, rule.IfExists)

Чтобы сохранять последние ошибки из лога, мы сделали функции errorf, errorln, которые ведут себя так же, как и функции fmt.Println, fmt.Printf, только сохраняют текст ошибки в errorHistory, а затем печатают сообщения на экран и в файл через логер Error.

func errorln(v …interface{}) {
s := fmt.Sprintln(v…)
Error.Print(s)
saveErrorHistory(s)
}
func errorf(format string, v …interface{}) {
s := fmt.Sprintf(format, v…)
Error.Println(s)
saveErrorHistory(s)
}
func saveErrorHistory(s string) {
tm := time.Now().Format(“2006–01–02 15:04:05”)
errorHistory = append(errorHistory, fmt.Sprintf(“%s %s”, tm, s))
if len(errorHistory) > histLength {
errorHistory = errorHistory[1:]
}
}

Теперь можно выдавать последние ошибки на страницу статистики.

 li := “”
for _, e := range errorHistory {
li += “<li>” + e + “</li>”
}
git checkout tags/ch06
go get
go build

Исходник можно посмотреть тут.

На этом у меня всё. Спасибо тем, кто дочитал до конца.

UPD1. В версии, которая на гитхабе тут сделал генерацию HTML с помощью html/template.

UPD2. Работа с логом тоже переписана.

Ссылки

  1. Репозиторий на github
  2. Эхопроцессор
  3. YAML
  4. JSON
  5. Пакет для работы с YAML
  6. go-strftime — пакет для (более привычного) форматирования времени
  7. Про мьютексы: раз, два

--

--