Предисловие:
В одной из первых статей я рассказывал про модернизацию нашей телекомпании. В ходе пуско-наладочных работ было не мало проблем, и я тогда упоминал лишь об одной из них. И вот пришло время поговорить еще об одной (как тогда показалось) мелочи. И о том, как эта мелочь выросла в целую историю.
А история эта началась вовремя приёмо – сдаточных испытаний. Когда у нас дошло дело до системы логгирования / регистрации эфира, интеграторы показали пальцем на очередной из множества сервер, со словами: «Это сервер полицейской записи. Пишет весь эфир 24/7». Конечно, удобно, надёжно, любой «чих» в эфире тут же записан и т.д. Но! Первый же логичный вопрос: а сколько данный сервер хранит записи? Давайте, для начала, обратимся к законодательству. Статья 34 Закона “О средствах массовой информации” гласит:
В целях обеспечения доказательств, имеющих значение для правильного разрешения споров, редакция радио-, телепрограммы обязана:
сохранять материалы собственных передач, вышедших в эфир в записи;
…
Сроки хранения:
материалов передач – не менее одного месяца со дня выхода в эфир;
…
Аудио- и видеозаписи вышедших в эфир радио- и телепрограмм, содержащих предвыборную агитацию, агитацию по вопросам референдума, хранятся в соответствующей организации, осуществляющей теле- и (или) радиовещание, не менее 12 месяцев со дня выхода указанных программ в эфир.
И так, вопрос по времени хранения материала, как наверно понимает дорогой читатель, ни разу не лишний. Суровые люди в пиджаках, в случае чего, могу не кисло наказать за несоблюдение такой статьи. Так вот, ответ от представителя интеграторов меня сильно удивил: «нууу… в зависимости от битрейта, разрешения и бла-бла-бла… где-то 2 – 3 месяца». Опа! Или кто-то не читал законов или кто-то пытается нас нагреть. И так, после недолгих переговоров между нами, головным предприятием и интеграторами, нас как филиал поставили перед фактом. Много слов было сказано, и про деньги, и про стандартизированные схемы, но пересказывать тут их смысла нет, не о том речь. В двух словах: е#$сь с этой x%$#ёй сам, сын мой! (ссылка на авторство фразы по картинке ниже)
Глава 1 «минус на минус дают костыли»
После приёмо – сдаточных испытаний, сели мы с инженерами думать, как решать эту проблему. Очевидное решение – наращивать память в сервере, но (крылатая фраза) денег нет, а диски там стоят SAS, что тоже не удешевляет решение. Значит нужно куда-то данные периодически перекачивать. И вот, ползая по спецификации «стандартизированного комплекса» я натыкаюсь на один из серверов вещания. Объем дискового массива – 28 Тб. Хм… путём не хитрых математических вычислений, понимаю, что такие объемы там просто не к чему. Да черт вас дери, что за «умы» выдумывали этот комплекс??? Откуда такие цифры? При наших объемах вещания, нам нужна от силы четверть! После того, как я потушил свой пылающий от негодования и недоумевания тухес, мы посчитали объем требуемого хранения полицейской записи, получили 12 Тб. Ну что же, вот нам и место! Осталось решить кто и как будет переносить данные? Ну нет, на человека вешать такие задачи – это моветон и вообще не инженерский подход. Значит ТЗ:
- Раз в сутки, чтобы не большими объемами, забирать порцию видеофайлов mp4 из папки – источника
- Выбирать только те файлы, что старше определенного времени, чтобы не плодить большую избыточность.
- В то же время, в папке назначения нужно проверять и удалять файлы старше определенного времени, чтобы не хранить лишнее.
- Естественно все действия нужно логгировать.
Глава 2 «Ну что, погнали кодить!»
Самое простое, что мне пришло в голову – C#. Дело я с ним ранее имел, да и примеров в интернете достаточно. Значит создаём проект и начинаем говнокодить.
И первое же, без чего мы не сможем обойтись, это где и как хранить настройки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
using System; using System.IO; using System.Xml.Serialization; namespace CopyPolice { public class settings { public string sorceDir = Directory.GetCurrentDirectory(); public string locationDir = Directory.GetCurrentDirectory(); public string targetPattern = "*.*"; public string intervalValue = "00:00:00"; public decimal daysOldValue = 1; public decimal liveTimeValue = 1; public int log_limit = 102400; public void Save(String file) { if (!Directory.Exists(Sett.getSettings.locationDir)) Directory.CreateDirectory(Sett.getSettings.locationDir); if (File.Exists(file)) File.Copy(file, file + ".bak", true); XmlSerializer serializer = new XmlSerializer(typeof(settings)); FileStream writer = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete | FileShare.Read | FileShare.Write); try { serializer.Serialize(writer, this); writer.Flush(); } catch { } if (writer != null) writer.Dispose(); } public settings Load(String file) { if (!File.Exists(file)) Save(file); XmlSerializer serializer = new XmlSerializer(typeof(settings)); settings po = new settings(); FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); try { po = (settings)serializer.Deserialize(fs); } catch { } if (fs != null) fs.Dispose(); return po; } } } |
Тут же прописываем функции чтения и записи настроек. В качестве файла с настройками у нас будет .xml файл. При каждом пересохранении будем его бэкапить, на всякий случай.
Значит настройки проверили, работает. Накидываем по-быстрому простенький интерфейс (незабываем про иконку! Без неё работать будет не по феншую):
Итого имеем:
- Указатель папки исходника
- Указатель дней, старше которых нужно забирать файлы
- Указатель типа файлов, на случай если в папке лежит еще куча всего. Мусор нам тянуть не нужно
- Указатель папки назначения (архива). Туда же будем класть файл с логами.
- Указатель дней, сколько нужно хранить файлы в архиве
- А также указатель времени выполнения задачи и размера файла логов. По истечении последнего, файл с логами будет зачищаться автоматически.
- Так же предусмотрим кнопку ручного копирования и ручного удаления. Это полезно при первом пуске программы, чтобы вручную перетянуть большой начальный объем данных.
Переходим к основному коду:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
using System; using System.IO; using System.Windows.Forms; using System.Globalization; namespace CopyPolice { public partial class mainForm : Form { FolderBrowserDialog dialog = new FolderBrowserDialog(); Timer Timer1 = new Timer(); private String fileSettings = @"settings.xml"; public mainForm() { InitializeComponent(); InitializeTimer(); } private void mainForm_Load(object sender, EventArgs e) { Sett.getSettings = Sett.getSettings.Load(fileSettings); sorcePath.Text = Sett.getSettings.sorceDir; locationPath.Text = Sett.getSettings.locationDir; fromTime.Text = Sett.getSettings.intervalValue; daysOld.Value = Sett.getSettings.daysOldValue; liveTime.Value = Sett.getSettings.liveTimeValue; pattern.Text = Sett.getSettings.targetPattern; loglimit.Text = Convert.ToString(Sett.getSettings.log_limit / 1024); logs.addSting("Приложение загружено", 1); StatusLabel.Text = "Мониторинг остановлен"; } private void mainForm_Closing(object sender, FormClosingEventArgs e) { Sett.getSettings.Save(fileSettings); } private void InitializeTimer() { Timer1.Interval = 1000; Timer1.Tick += new EventHandler(Timer1_Tick); } private void Timer1_Tick(object Sender, EventArgs e) { DateTime d1 = DateTime.Now; DateTime d2 = DateTime.ParseExact(Sett.getSettings.intervalValue, "HH:mm:ss", CultureInfo.CurrentCulture); StatusLabel.Text = d1.ToString("HH:mm:ss"); if (d1.Second == d2.Second && d1.Minute == d2.Minute && d1.Hour == d2.Hour) { logClearing(); ProcessCopy(); ProcessDelete(); } } private void sorceEdit_Click(object sender, System.EventArgs e) { DialogResult result = dialog.ShowDialog(); if (result == DialogResult.OK) { sorcePath.Text = dialog.SelectedPath; Sett.getSettings.sorceDir = dialog.SelectedPath; } } private void locationEdit_Click(object sender, System.EventArgs e) { DialogResult result = dialog.ShowDialog(); if (result == DialogResult.OK) { locationPath.Text = dialog.SelectedPath; Sett.getSettings.locationDir = dialog.SelectedPath; } } private void sorceEdit_Change(object sender, System.EventArgs e) { Sett.getSettings.sorceDir = sorcePath.Text; } private void locationEdit_Change(object sender, System.EventArgs e) { Sett.getSettings.locationDir = locationPath.Text; } private void targetPattern_Change(object sender, System.EventArgs e) { Sett.getSettings.targetPattern = pattern.Text; } private void fromTime_Change(object sender, System.EventArgs e) { Sett.getSettings.intervalValue = fromTime.Text; } private void daysOld_Change(object sender, System.EventArgs e) { Sett.getSettings.daysOldValue = daysOld.Value; } private void liveTimeValue_Changed(object sender, EventArgs e) { Sett.getSettings.liveTimeValue = liveTime.Value; } private void loglimit_Change(object sender, System.EventArgs e) { Sett.getSettings.log_limit = Convert.ToInt16(loglimit.Text) * 1024; } private void startButton_Click(object sender, System.EventArgs e) { if (Timer1.Enabled == true) { sorcePath.Enabled = true; locationPath.Enabled = true; sorceEdit.Enabled = true; locationEdit.Enabled = true; pattern.Enabled = true; daysOld.Enabled = true; liveTime.Enabled = true; fromTime.Enabled = true; loglimit.Enabled = true; manStart.Enabled = true; manDel.Enabled = true; startButton.Text = "АВТОМАТ"; Timer1.Enabled = false; logs.addSting("Мониторинг остановлен", 1); StatusLabel.Text = "Мониторинг остановлен"; } else { Sett.getSettings.Save(fileSettings); sorcePath.Enabled = false; locationPath.Enabled = false; sorceEdit.Enabled = false; locationEdit.Enabled = false; pattern.Enabled = false; daysOld.Enabled = false; liveTime.Enabled = false; fromTime.Enabled = false; loglimit.Enabled = false; manStart.Enabled = false; manDel.Enabled = false; startButton.Text = "СТОП"; Timer1.Enabled = true; logs.addSting("Мониторинг запущен", 1); StatusLabel.Text = "Мониторинг запущен"; } } private void manStart_Click(object sender, EventArgs e) { logClearing(); Sett.getSettings.Save(fileSettings); logs.addSting("Ручное копирование", 1); StatusLabel.Text = "Ручное копирование"; ProcessCopy(); } private void manDel_Click(object sender, EventArgs e) { logClearing(); Sett.getSettings.Save(fileSettings); logs.addSting("Ручное удаление", 1); StatusLabel.Text = "Ручное удаление"; ProcessDelete(); } private void logClearing() { if (Sett.getSettings.log_limit != 0) { FileInfo size_sys = new FileInfo(Sett.getSettings.locationDir + "/logs.txt"); long sys = size_sys.Length; if (sys >= Sett.getSettings.log_limit) { logs.clr(); logs.addSting("Логи системы были переполнены и очищены", 1); StatusLabel.Text = "Логи системы были переполнены и очищены"; } } } private void ProcessCopy() { try { string[] fileEntries = Directory.GetFiles(Sett.getSettings.sorceDir, Sett.getSettings.targetPattern); foreach (string fileName in fileEntries) Copy(fileName); } catch { logs.addSting("Не найден путь " + Sett.getSettings.sorceDir, 3); StatusLabel.Text = "Не найден путь " + Sett.getSettings.sorceDir; } } private void Copy(string path) { DateTime modification = File.GetLastWriteTime(path); DateTime now = DateTime.Now; TimeSpan diference = now - modification; string fName = Path.GetFileName(path); string dest = Path.Combine(Sett.getSettings.locationDir, fName); if (diference.Days >= Sett.getSettings.daysOldValue && !File.Exists(dest)) { try { File.Copy(path, dest); logs.addSting(fName + " скопирован", 2); StatusLabel.Text = fName + " скопирован"; } catch { logs.addSting(fName + " ошибка копирования", 3); StatusLabel.Text = fName + " ошибка копирования"; } } } private void ProcessDelete() { try { string[] fileEntries = Directory.GetFiles(Sett.getSettings.locationDir); foreach (string fileName in fileEntries) Delete(fileName); } catch { logs.addSting("Не найден путь " + Sett.getSettings.locationDir, 3); StatusLabel.Text = "Не найден путь " + Sett.getSettings.locationDir; } } private void Delete(string path) { DateTime modification = File.GetLastWriteTime(path); DateTime now = DateTime.Now; TimeSpan diference = now - modification; string fName = Path.GetFileName(path); if (diference.Days >= Sett.getSettings.liveTimeValue) { try { File.Delete(path); logs.addSting(fName + " удален", 2); StatusLabel.Text = fName + " удален"; } catch { logs.addSting(fName + " ошибка удаления", 3); StatusLabel.Text = fName + " ошибка удаления"; } } } } } |
Тут всё просто. Вешаем таймер и сравниваем в нём текущее время с тем, что указали в настройках. Как только время пришло, выполняем функции чистки логов, копирование и удаления файлов, не забывая в логи прописать все свои действия.
Функция очистки логов проверяет, достигнут ли объем указанный в настройках, и если да, то зачищает файл. Эта функция специально идёт первая, чтобы не затирать последние свои действия. Т.е. если объем логов превышен – зачищаем их, пишем в лог об этом, а потом пишем туда какие файлы и когда были удалены и скопированы.
Функция копирования находит все файлы в указанной папке, выбирает и копирует из них по очереди все файлы, соответствующие условиям (расширение и дата изменения).
Функция удаления производит все те же итерации что и предыдущая функция, но со своими параметрами и в папе архива.
Заключение «CopyPolice»
Программа CopyPolice была написана за 3-4 дня и «подвешена» на сервере вещания. Выбрано время, когда сервер простаивал и произведен начальный перенос данных вручную. После этого программа сидит в памяти 24/7, потребляя 3-4 Мб оперативной памяти, и в назначенный час выполняет свои манипуляции. Недостаток в ходе года эксплуатации выявил только один: когда программа выполняет процессы копирования и удаления, интерфейс перестает отвечать на это время и программа как бы «подвисает». Конечно, это можно исправить, но это уже как обычно – когда дойдут руки. Ну а для тебя, мой самый усидчивый читатель, сама программа с исходниками во вложении к этой статье внизу. Надеюсь кому то она еще пригодится. И по традиции, пользуйся своими мозгами правильно, дорогой читатель.