#!/usr/bin/env ruby
# encoding: utf-8
# :title: Чистильщик медиа-архива AVReg
#--
# avreg-unlink - AVReg archive cleaner (чистильщик архива)
#
# Copyright (C) 2007-2014 "Setevye informacionnye sistemy" LTD, Russia
#                        ("Network information systems")
# Author: Andrey Nikitin <nik-a at mail.ru>
#
# License: GPLv3
#++
# === Чистильщик медиа-архива AVReg
# Модуль avreg-unlink представляет собой исполняемый ruby-скрипт,
# который непосредственно вызывается для очистки архива AVReg.
#
# ==== Как обеспечивается цикличность (кольцевого) медиа-архива AVReg.
#
# Демон avregd совершенно не следит за свободным местом на блочном устройстве
# на которое указывает параметр "storage-dir" конфигурационного файла avreg.conf
# Если не удалять старые записи, в какой то момент avregd остановится с ошибкой
# ENOSPC "no space left of device".
# Задачу удаления старых записей выполняет avreg-unlink.

require 'gettext'
include GetText
bindtextdomain('avreg-unlink')
include ObjectSpace

require 'pp'

$pr_name='avreg-unlink'
$version='6.3p2'
pid_name="/var/run/#{$pr_name}.pid"

$verbose = 0
$quit = false
$errnct = 0

require 'avreg/utils'
include  AVRegUtils
# include  AVRegUtils::TTY

# Подгружаем модули
$:.insert(0,'.') if File.exist?('./avreg/unlink/common.rb')
require 'avreg/unlink/logger.rb'
require 'avreg/unlink/common.rb'
require 'avreg/unlink/conf.rb'
require 'avreg/unlink/db.rb'
require 'avreg/unlink/fs.rb'

include AVRegUnlinkConf

$log = AVRegUnlinkLogger::Logger.new($pr_name)

##
# Читаем опции командной строки и конфигурационного файла.
# Приоритет имеют опции коммандной строки.
#
# В общем виде возвращается массив структур (хешей).
# Каждый элемент массива - конфигурация конкретного профиля AVReg.
# Если профили не используются - структура параметров в массиве единственная.
#
# Подробнее о профилях - http://avreg.net/manual_applications_multi-user.html
all_profiles_opts = AllProfilesOpts.new(ARGV)

if not File.exist?(pid_name)
   pidf = File.new(pid_name, File::CREAT|File::WRONLY, 0644)
else
   pidf = File.new(pid_name,"r+");
   another_pid = pidf.gets
   another_pid.chomp! if not another_pid.nil? and not another_pid.empty?
   pid_was_create = true
end

if not pidf.flock(File::LOCK_EX | File::LOCK_NB)
   if another_pid.nil? or another_pid.empty?
      pscmd = "ps u -C #{$pr_name}"
   else
      pscmd = "ps u -p #{another_pid}"
   end
   $log.err "Program \"%{pr_name}\" already processed, see `%{pscmd}'" % {:pr_name => $pr_name, :pscmd => pscmd}
   exit 1
end

if not pid_was_create.nil?
   pidf.truncate(0)
   pidf.seek(0)
   pidf.puts Process.pid.to_s
   pidf.flush
end

Process.setpriority(Process::PRIO_PROCESS, 0, 19)   #=> 0

##
# exit signals handler
def OnExit(signo)
   $log.notice _('Got signal') + ' #'+ signo.to_s + ' ' + Signal.list.key(signo) + '. ' + _('Terminating') + '...' if $verbose
   $quit = true
end

# устанавливаем обработчики сигналов,
# чтобы по "неожиданному" завершению
# обеспечить целостность и соответствие базы (ссылок на файлы)
# и файлов в файловой системе
trap(:INT)  { |signo| OnExit(signo) }
trap(:TERM) { |signo| OnExit(signo) }
trap(:QUIT) { |signo| OnExit(signo) }
trap(:PWR)  { |signo| OnExit(signo) }

trap(:USR1, 'IGNORE')
trap(:USR2, 'IGNORE')
trap(:TTIN, 'IGNORE')
trap(:TTOU, 'IGNORE')
#trap(:ALRM, 'IGNORE')

##
# Время зауска программы.
#
# Используется в основном для того, чтобы ограничить время выборки
# и не удалять только что записанные файлы, см. параметр "select-now-minus"
# Дело в том, что удаление очередной порции ссылок в таблице EVENTS
# происходит после выборки порции, разумеется, и физического удаления файлов
# из файловой системы. В итоге, к моменту удаления из базы выбранной ранее порции ссылок
# avregd может назаписывать файлов времена которых попадают под выборку,
# однако они не были выбраны, но будут удалены, а файлы в ФС останутся "брошенными" навечно.
start_time = Time.now

profiles = Array.new

save_euid = Process.euid
save_egid = Process.egid

# prevent chdir(/root) by run from cron
Dir.chdir('/')

##
# По всем профилям если используются или одному единственному
# выполняются рабочие операции программы
all_profiles_opts.each do | opts |
   if all_profiles_opts.length > 1
      $log.reopen(opts['profile'])
      $log.info _("database \"%{db}\", storage-dir: `%{dir}'") %
                   { :db => opts['db-name'], :dir => opts['storage-dir']}
   end
   pp(opts) if $verbose >= 2

   # setegid()/seteuid()
   if opts['gid']
      Process::Sys.setegid(opts['gid'])
      $log.debug("setegid(#{opts['gid']}/#{opts['group']})")
   end
   if opts['uid']
      Process::Sys.seteuid(opts['uid'])
      $log.debug("seteuid(#{opts['uid']}/#{opts['user']})")
   end

   begin
      ##
      # "Открываем" хранилище
      storage = Storage.new(opts)

      ##
      # Последовательно выполняют рабочие операции с хранилищем:
      #  - repair_db   - восстановить базу данных,
      #                  выполняется если задано конфигом или командной строкой.
      #  - unlink      - чистить архив.
      #  - optimize_db - оптимизировать базу данных,
      #                  выполняется если задано конфигом или командной строкой.
      #  - close       - отключится от хранилища (выполняется безусловно).
      #
      %w[ repair_db unlink optimize_db close! ].each do | operation |
         storage.method(operation).call
      end
   rescue  => detail
      $log.debug(detail.backtrace[0] + ': (' + detail.class.to_s + ')')
      next
   ensure
      if opts['uid']
         Process::Sys.seteuid(save_euid)
         $log.debug("restore euid")
      end
      if opts['gid']
         Process::Sys.setegid(save_egid)
         $log.debug("restore egid")
      end
   end
end

pidf.close
File.unlink(pid_name)

jobs_sec = (Time.now - start_time).to_i
$log.notice _('Jobs time: ') + jobs_sec.to_duration if jobs_sec > 1

$log.close

exit $errnct
