16 января 2016 г.

Автозапуск Telegram при загрузке Ubuntu 14.04

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

Для этого открываем панель Unity и запускаем окно управления автозапуском программ:
открываем автоматически запускаемые приложения
Нажимаем "Добавить":
в появившемся окне нажимаем "Добавить"
Вводим имя программы, и, самое главное, путь к бинарнику и опцию -startintray. Описание вводить не обязательно:
в поле имя пишем Telegram, в поле команда пишем путь до телеграма и опцию -startintray
Нажимаем добавить и telegram появился среди автоматически запускаемых приложений:
среди приложений должен появится telegram
Теперь необходимо выйти из текущего сеанса и зайти заново. После входа у нас в трее появится значок Telegram:
Мной был замечен глюк не глюк, а баг Issue 422, что окно может не открыться с первого раза, чтобы оно открылось нужно будет нажать "Open Telegram", потом жмем "Minimize to tray", и снова "Open Telegram". Баг, судя по всему, исправлять не торопятся, к сожалению.

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

4 октября 2015 г.

Исправляем проблему сборки модулей VirtualBox под Ubuntu

Который раз уже сталкиваюсь с проблемой, что срочно нужна виртуальная машина с WinXP, но быстро выясняется, что после обновления ядра DKMS не собрал необходимые модули автоматически. Поэтому приходилось вручную собирать модули следующей командой:
sudo /etc/init.d/vboxdrv setup
По сообщениям в консоли, стало понятно, что регистрация модулей через DKMS не срабатывает, поэтому после установки обновлений ядра автоматическая установка не происходит, что подтвердил вывод команды:
dkms status | grep vboxhost
Немного погуглив по английским сайтам, нашел рабочее решение, чтобы восстановить сборку через dkms нужно сделать следующее:
sudo rm -rf /var/lib/dkms/vboxhost/
sudo dpkg-reconfigure virtualbox-5.0
Первой командой мы удаляем настройки от старых версий virtualbox (а у меня их была целая куча), затем перенастраиваем пакет с virtualbox (здесь нужно будет указать свою версию virtualbox, если вы пользуетесь отличной от моей). После чего вас могут попросить закрыть виртуалбокс, а так же спросят, нужно ли установить драйвер прямо сейчас - отвечаем ДА. После чего через dkms соберется драйвер для текущей версии ядра. Убедиться в том, что все прошло успешно, можно командой:
dkms status | grep vboxhost
При успехе получим следующее:
vboxhost, 5.0.4, 3.16.0-50-generic, x86_64: installed
Обратите внимание, что версия ядра должна совпадать с текущим. На этом все, надеюсь кому-то эта информация пригодится.

Arduino + Raspberry Pi = термометр + гигрометр с отправкой данных на сервер (часть 2)

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

Для сбора данных с Arduino используется программа на языке Python, который читает данные в формате CSV с серийного порта ардуины, затем обрабатывает их и отправляет на веб-сервер, FTP и OpenWeatherMap. Так же я предусмотрел возможность записи в журнал сообщений о действии программы или об возникающих ошибках, поэтому можно легко понять, из-за чего данные могли не дойти до серверов.

Теперь рассмотрим поподробнее работу нашего сборщика данных. Удобнее всего будет давать объяснения прямо по тексту программы, поэтому начнем:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# тут мы импортируем все необходимые библиотеки и модули для дальнейшей работы
import serial
from serial import SerialException
import datetime
import re
import ftplib
import socket
import os
import sys
import time
import signal
import logging
from logging.handlers import WatchedFileHandler
import threading
import requests
from requests.exceptions import *

# тут у нас указан путь к файлу журнала, куда будут записываться все сообщения
LOG_PATH = '/var/log/mstation.log'
# формат сообщения, в конце статьи приведу ссылку на описание, что тут что означает
LOG_FORMAT = '[%(asctime)s] %(levelname)s: %(funcName)s: %(message)s'
# путь к серийному порту (наша ардуина)
SERIAL_DEV = '/dev/ttyACM0'
# здесь указываем имя пользователя, логин и пароль для ftp сервера
FTP_SERV = 'ftp_serv'
FTP_NAME = 'ftp_name'
FTP_PASS = 'ftp_password'
# путь к директории, в которой будут храниться полученные данные (на Raspberry Pi)
DATA_PATH = '/usr/local/MStation/data/'
# количество попыток установить соединение с ардуиной
CONNECT_TRYOUT = 10
# путь к php файлу на сервере, который обрабатывает отправленные данные
UPLOAD_URL = 'upload_handler'
# OpenWeatherMap
# данные для проекта OpenWeatherMap (логин, пароль, название станции и ее координаты)
URL_OPENW = 'http://openweathermap.org/data/post'
LATITUDE = 56.3294
LONGITUDE = 37.7532
ALTITUDE = 215
USER_NAME = 'user_name'
PASSW = 'password'
STATION_NAME = 'station_name'
# OpenWeatherMap

# блокировки для корректной работы потоков, подробности будут ниже
write_lock = threading.Lock()
upload_lock = threading.Lock()

# очередь с пакетами, которые не удалось отправить
failed_upload = []

# тут мы настраиваем работу логгера
log = logging.getLogger('main_log')
log.setLevel(logging.INFO)
log_handler = WatchedFileHandler(LOG_PATH)
log_fmt = logging.Formatter(fmt=LOG_FORMAT)
log_handler.setFormatter(log_fmt)
log.addHandler(log_handler)


# основная функция, отсюда начинается выполнение
def main():
    signal.signal(signal.SIGINT, ctrl_c_handler)
    # генерируем имя файла, в который будут записаны полученные от Arduino данные
    f_name = datetime.datetime.now().strftime('%Y-%m-%d.cvs')
    # получаем полный путь к файлу
    f_path = DATA_PATH + f_name
    # открываем файл в режиме присоединения, т.е. если файл уже есть на диске, то данные будут в него дописываться
    f_table = open(f_path, 'a', 0)

    # открываем соединение с Arduino
    ser = serial_connect(SERIAL_DEV, 115200)

    # паттерн для отсеивания строки инициализации
    init_patt = re.compile(r'MStation')
    # запоминаем текущую дату
    curr_date = datetime.date.today()

    # считывание данных у нас происходит в бесконечном цикле
    while True:
        try:
            # считываем строку из порта пока не встретится символ окончания строки
            curr = ser.readline()
        except SerialException as e:
            # если возникла ошибка, то открываем соединение заново
            log.error('Serial error: ' + str(e))
            time.sleep(120)
            ser = serial_connect(SERIAL_DEV, 115200)
            continue

        # проверяем, считанная строка - строка инициализации ?
        init_match = re.search(init_patt, curr)

        if init_match:
            # если да, то отправляем ее в журнал
            log.info(curr)
            continue
        # если у нас изменилась дата, то меняем имя файла и открываем новый файл
        if curr_date.day != datetime.date.today().day:
            f_table.close()
            f_name = datetime.datetime.now().strftime('%Y-%m-%d.cvs')
            f_path = DATA_PATH + f_name
            f_table = open(f_path, 'a', 0)
            curr_date = datetime.date.today()

        # приписываем к строки с данными текущие дату и время
        date_str = datetime.datetime.now().strftime('%Y/%m/%d %H:%M')
        final_str = date_str + ',' + curr

        # создаем поток, который отсылает данные на ftp сервер
        ftp_thread = threading.Thread(
            target=upload_to_ftp,
            args=(FTP_SERV, FTP_NAME, FTP_PASS, f_name)
        )
        # создаем поток, который отсылает данные на веб сервер
        upl_thread = threading.Thread(
            target=upload_to_site,
            args=(UPLOAD_URL, final_str)
        )
        # создаем поток, который отсылает данные на проект OpenWeatherMap сервер
        openw_thread = threading.Thread(
            target=upload_to_openweathermap,
            args=[final_str]
        )

        # записываем данные в файл
        write_lock.acquire()
        f_table.write(final_str)
        write_lock.release()
        # запускаем работу потоков
        ftp_thread.start()
        upl_thread.start()
        openw_thread.start()

    sys.exit(0)


# функция для создания соединения с Arduino через серийный порт
def serial_connect(dev, speed):
    try_count = 1

    while try_count <= CONNECT_TRYOUT:
        try:
            ser = serial.Serial(dev, speed)
        except SerialException as e:
            log.error('Serial Connect Error: ' + str(e))
            print 'Connection Error'
            time.sleep(120)
        else:
            log.info('Connected after ' + str(try_count) + ' tryouts')
            return ser
        finally:
            try_count += 1

    log.critical('FAIL to connect. Exit.')
    sys.exit(-1)


# функция для отправки данных на ftp сервер
def upload_to_ftp(host, name, passw, f_name):
    # генерируем полный путь к файлу
    path = DATA_PATH + f_name
    # открываем файл
    upl_file = open(path, 'rb')
    # к файлу на сервере при загрузке дописывается .new для того, чтобы в случае проблем с соединением, на сервере не оказался испорченный файл, поэтому файл заливается с приписанным к нему .new, а при успехе файл переименовывается
    name_new = f_name + '.new'
    try:
        # устанавливаем соединение и загружаем файл в папку public_html
        ftp_serv = ftplib.FTP(host, name, passw, '', 30)
        ftp_serv.cwd('public_html')
        ftp_serv.voidcmd('TYPE I')
        ftp_serv.voidcmd('PASV')
        write_lock.acquire()
        ftp_serv.storbinary('STOR '+name_new, upl_file)
        # смотрим размер залитого файла
        upl_size = ftp_serv.size(name_new)
        if upl_size:
            # сравниваем с файлом на диске, при совпадении переименовываем на сервере
            f_size = os.path.getsize(path)
            if upl_size == f_size:
                ftp_serv.rename(name_new, f_name)
            else:
                raise uplFail('sizes not match')
        else:
            raise uplFail('unknown error')
        ftp_serv.close()
    # обработка потенциальных ошибок
    except (socket.error, socket.gaierror) as e:
        log.error('FTP Socket Error: ' + str(e))
    except ftplib.error_temp as e:
        log.error('FTP Temporary Error: ' + str(e))
    except ftplib.error_perm as e:
        log.error('FTP Permanent Error: ' + str(e))
    except ftplib.error_reply as e:
        log.error('FTP Unexpected Error: ' + str(e))
    except uplFail as e:
        log.error('FTP Upload Failed: ' + str(e))
    else:
        log.info('FTP Upload Success')
    finally:
        if write_lock.locked():
            write_lock.release()

    upl_file.close()


# необходим для работы загрузки по FTP
class uplFail(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


# отправка данных на веб-сервер
# при запуске отправляет один пакет на сервер, при успехе смотрит, не остались ли неотправленные данные, если остались, то пытается их отослать, если отправка одного пакета оказалась неудачной - добавляет пакет в список неотправленных пакетов
def upload_to_site(url, data_str):
    if not upload_to_url(url, data_str):
        failed_upload.append(data_str)
    else:
        if upload_lock.acquire(False):
            while len(failed_upload) != 0:
                data = failed_upload[0]
                if upload_to_url(url, data):
                    failed_upload.pop(0)
            upload_lock.release()


# отправляет один пакет на сервер
def upload_to_url(url, data_str):
    # подготавливаем данные к отправке
    data_send = data_str.split(',')
    data_req = {
        'date': data_send[0],
        'temp1': data_send[1],
        'temp2': data_send[2],
        'humid': data_send[3]
    }

    try:
        # пытаемся отправить данные на веб-сервер
        req = requests.post(url, data=data_req, timeout=60)
    # обрабатываем ошибки, если они были
    except (ConnectionError, HTTPError, URLRequired, Timeout) as e:
        log.error(str(e))
        return False
    else:
        # проверяем, правильно ли обработан наш POST запрос
        if (req.status_code == requests.codes['ok']):
            log.info('Upload success.')
            return True
        else:
            log.error('Upload failed with code ' + str(req.status_code))
            return False


# отправка данных на проект OpenWeatherMap, работает аналогично предыдущей функции
def upload_to_openweathermap(data_str):
    data_send = data_str.split(',')
    data_post = {
        'temp': data_send[1],
        'humidity': data_send[3],
        'lat': LATITUDE,
        'long': LONGITUDE,
        'alt': ALTITUDE,
        'name': STATION_NAME
    }
    try:
        req = requests.post(
            URL_OPENW,
            auth=(USER_NAME, PASSW),
            data=data_post,
            timeout=60
        )
    except (ConnectionError, HTTPError, URLRequired, Timeout) as e:
        log.error(str(e))
    else:
        if (req.status_code == requests.codes['ok']):
            log.info('Upload to OpenWeather success.')
        else:
            log.error(
                'Upload to OpenWeather failed with code '
                +
                str(req.status_code)
            )


# обработчик сигнала об завершении работы программы
def ctrl_c_handler(signum, frame):
    print 'Exit.'
    log.info('Exit.')
    logging.shutdown()
    sys.exit(0)

if __name__ == '__main__':
    main()

Теперь необходимо рассказать о некоторых аспектах работы всей этой штуковины. Как уже говорилось в предыдущей части статьи, данные с ардуины приходят в формате CSV, здесь же к этим данным дописываются текущие дата и время, естественно с условием соблюдения этого формата. Все эти данные хранятся на диске (в случае с Raspberry Pi на sd карточке), и при каждом изменении файла отправляются на FTP-сервер. Для чего ? Все довольно просто, изначально данные на сервере брались из файла, но впоследствии я реализовал хранение в бд MySQL, а реализованную загрузку на ftp решил не удалять. Если вам она не нужна, то просто удалите код, связанный с FTP. И да, один файл содержит данные, собранные за сутки, поэтому называется, к примеру, 2015-10-01.cvs, а его содержимое выглядит примерно вот так:
2014/09/05 14:35,20.57,21.90,50.80
2014/09/05 14:40,20.66,21.90,50.00
2014/09/05 14:45,20.51,22.00,49.90
2014/09/05 14:50,20.98,22.00,50.00
2014/09/05 14:55,20.98,22.00,50.00
Данные на сервер отправляются POST запросом (для собственного веб-сервера и для OpenWeatherMap). О том, что этот запрос из себя представляет, почитайте вот тут: http://www.myfirstsite.ru/articles/get-and-post 
Так как я пользуюсь потоками для отправки данных на сервера, то возникла необходимость ввести примитивы синхронизации потоков, такие как блокировки:
write_lock = threading.Lock()
upload_lock = threading.Lock()
Первая блокировка нужна для того, чтобы у нас не возникло ситуации, что мы записываем данные в файл, а в этот момент у нас происходит его отправка на сервер, без блокировки мы бы получили файл, в конце которого был бы кусок данных, что не очень то хорошо. Вторая блокировка нужна, чтобы у нас не возникло ситуации, что у нас одновременно работают два потока и отправляют на сервер одни и те же данные. Подробнее об этих механизмах потоков и блокировках почитайте вот тут: https://www.ibm.com/developerworks/ru/library/l-python_part_9/

В конце скажу, что автозапуск программы осуществляется через upstart, подробно об этом я писал у себя вот в этой статье: Raspberry Pi + Raspbian = автозапуск программы при помощи upstart после синхронизации с NTP

На этом, пожалуй, и остановимся, в следующей части статьи расскажу о той части, которая работает на стороне сервера и обрабатывает запросу на прием и выдачу данных. Если возникнут какие-то вопросы, пишите мне в G+ или в комментарии.

Всего вам доброго.

Ссылка на исходный код: https://github.com/edgar-ch/mstation/blob/stable/mstation_serv.py
Предыдущая часть статьи: http://blog.edtex.ru/2014/09/arduino-raspberry-pi-1.html

12 сентября 2015 г.

Установка Redmine под Ubuntu 12.04

Недавно понадобилось установить Redmine для разработки всяких плагинов, и этот рпоцесс прошел с довольно-таки неплохими корчами. Кроме самого редмайна понадобится установить еще базу данных и сервер. Поехали.


Для начала нужно установить пакеты с бд, сервером и прочим стафом, который нам понадобится:       
sudo aptitude install mysql-server mysql-client libmysqlclient-dev gcc build-essential 
                      zlib1g zlib1g-dev zlibc ruby-zip libssl-dev libyaml-dev 
                      libcurl4-openssl-dev ruby gem libapache2-mod-passenger 
                      apache2-mpm-prefork apache2-dev libapr1-dev libxslt1-dev 
                      checkinstall libxml2-dev ruby-dev vim
 
Теперь скачаем и подготовим сам редмайн:
cd /opt/
sudo mkdir redmine
sudo chown -R $your_user redmine
cd redmine
wget $redmine.tar.gz
tar xzf $redmine.tar.gz
cd redmine-X.X.X
Если что-то работает не так, всегда можно распоковать менеджером архивов ;)

Теперь подготовим MySQL:
mysql --user=root --password=$password
CREATE DATABASE redmine CHARACTER SET utf8;
CREATE USER 'redmine'@'localhost' IDENTIFIED BY 'my_password';
GRANT ALL PRIVILEGES ON redmine.* TO 'redmine'@'localhost';
exit
Вместо my_password здесь и далее не забудьте указать ваш пароль.
Сделаем конфиг для рейдмайн бд и попарвим его:
cp config/database.yml.example config/database.yml
В самом файле config/database.yml запишем: 
production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: redmine
  password: my_password 
Пора заняться бандлером. Установим его:
sudo gem install bundler
bundle install --without development test rmagick
Сгенерируем secret token:
bundle exec rake generate_secret_token
Подготовим бд и проинсталируем все таблицы:
RAILS_ENV=production bundle exec rake db:migrate
RAILS_ENV=production bundle exec rake redmine:load_default_data
Пора протестить редмайн. Вместо $IP введите ваш внешний IP:
bundle exec ruby bin/rails server -b $IP webrick -e production
и посетите адрес http://$IP:3000.
Время для сервера. В нашем случае - Apach.

Apache работает с данными через www-data, так что нам нужно будет дать ей доступ к файлам:
sudo chown -R www-data files log tmp public/plugin_assets
sudo chmod -R 755 files log tmp public/plugin_assets
Создадим ссылку между публичной папкой редмайна и рутом апача:
sudo ln -s /opt/redmine/redmine-X.X.X/public/ /var/www/html/redmine
Создадим новый конфиг VirtalHost:
sudo vim /etc/apache2/sites-available/master.conf
и запишем в этот файл следующее для редмайна:
<VirtualHost *:80>

ServerAdmin admin@example.com
Servername hostname
DocumentRoot /var/www/html/

        <Location /redmine>
                RailsEnv production
                RackBaseURI /redmine
                Options -MultiViews
        </Location>

</VirtualHost>
Сохраните и закройте вим. Чтобы это сделать в командной строке вима нужно прописать :q. Я недавно узнала, что некоторые от отчаяния перезагружают комп, чтобы закрыть вим. Не надо так)

Отключаем дефолтный виртуал-хост:
sudo a2dissite 000-default.conf
И подрубаем новый мастер виртуал-хост:
sudo a2ensite master.conf
Чтобы избежать ошибки доступа, пассенджер мод тоже нужно запускать как www-data. В файле /etc/apache2/mods-available/passenger.conf добавьте эту строку:
PassengerUser www-data
И перезапустите апач:
sudo service apache2 restart
Посетите http://$IP/redmine - счастье откроется вам.

7 сентября 2015 г.

Сканирование локальной сети (IP и MAC адреса)

Недавно возникла необходимость узнать IP и MAC адрес raspberry pi, которая была подключена к роутеру по ethernet с ноутбука, который подключен к тому же роутеру, но по wifi. Наиболее удобной оказалась утилита arp-scan, которая рассылает по сети ARP пакеты и отображает данные о хостах, от которых был получен ответ. Запускаем ее из консоли со следующими опциями:

sudo arp-scan --interface wlan0 --localnet

Для ее работы необходимы привилегии root, поэтому используем sudo. В результате работы получим список хостов в локальной сети с их IP и MAC адресами:
результаты работы arp-scan
Таким образом, можно быстро узнать адрес raspberry pi в сети (как и любого другой платы или компьютера). В опции --interface указываем интерфейс, на котором нужно произвести сканирование.

5 сентября 2015 г.

Клонирование SD/microSD карты для Raspberry PI

Возникла необходимость сделать полную копию microSD карты с дистрибутивом Raspbian, не потеряв настройки и установленные программы. Мне копия понадобилась, чтобы переехать на большую шуструю карточку, да и резервная копия тоже не помешает.
Под Ubuntu есть удобная утилитка dd, которая позволяет делать копию устройства побайтно (копируются не файлы в отдельности, а целиком вся файловая система, включая так же и таблицу разделов, по сути получается клон sd карточки). Ей мы и воспользуемся. Работа с ней выглядит следующим образом (естественно в консоли):

dd if=/dev/sdX of=/path/to/image bs=1M

If у нас указывает на путь к устройству, с которого мы будем делать копию, а of указывает на путь образа, куда будет сохранена копия, bs указывает на размер блока, который будет считываться за один раз, в данном случае 1 мегабайт.
Чтобы узнать путь к нужному устройству, введем команду lsblk в консоли:

NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    0 298,1G  0 disk
├─sda1   8:1    0   100G  0 part
├─sda2   8:2    0    15G  0 part
├─sda3   8:3    0     1K  0 part
├─sda4   8:4    0    16M  0 part
├─sda5   8:5    0    17G  0 part /
├─sda6   8:6    0 161,1G  0 part /home
└─sda7   8:7    0     5G  0 part [SWAP]
sdb      8:16   1   3,8G  0 disk
├─sdb1   8:17   1    56M  0 part /media/edgar/boot
└─sdb2   8:18   1   3,7G  0 part /media/edgar/13d368bf-6dbf-4751-8ba1-88bed06bef


Чтобы понять, какое устройство нужное, смотрим на размер и путь, по которому примонтирована файловая система (если не работает автомонтирование, ориентируемся на размер). Тут у нас размер 3.8G, это как раз наша microSD карточка на 4 гигабайта. Так же тут видно, что raspbian использует отдельный раздел для boot, где лежит все необходимое для успешной загрузки. В итоге команда будет выглядеть вот так:

sudo dd if=/dev/sdb of=/home/edgar/raspbian_2015-09-04.img bs=1M

Для работы dd нужно привилегии root-пользователя, поэтому используем sudo. В конце работы утилитка выдала следующее:

3840+0 записей получено
3840+0 записей отправлено
скопировано 4026531840 байт (4,0 GB), 392,461 c, 10,3 MB/c


Тут у нас видно сколько было скопировано и на какой скорости. Теперь зальем образ на другую карточку. Отключим уже скопированную карточку и вставим новую, после чего еще раз запустим lsblk:

NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    0 298,1G  0 disk
├─sda1   8:1    0   100G  0 part
├─sda2   8:2    0    15G  0 part
├─sda3   8:3    0     1K  0 part
├─sda4   8:4    0    16M  0 part
├─sda5   8:5    0    17G  0 part /
├─sda6   8:6    0 161,1G  0 part /home
└─sda7   8:7    0     5G  0 part [SWAP]
sdb      8:16   1   7,4G  0 disk
└─sdb1   8:17   1   7,4G  0 part /media/edgar/6632-3961

Новая карточка у нас имеет размер 8 гигабайт, и находится она по пути /dev/sdb. Теперь запустим копирование образа на эту карточку:

sudo dd if=/home/edgar/raspbian_2015-09-04.img of=/dev/sdb bs=1M

В результате получим:

3840+0 записей получено
3840+0 записей отправлено
скопировано 4026531840 байт (4,0 GB), 429,094 c, 9,4 MB/c

Таким образом, наш образ скопирован на новую карточку. Теперь отключим и заново вставим карточку и убедимся, что образ успешно скопирован (через lsblk):

sdb      8:16   1   7,4G  0 disk
├─sdb1   8:17   1    56M  0 part /media/edgar/boot
└─sdb2   8:18   1   3,7G  0 part /media/edgar/13d368bf-6dbf-4751-8ba1-88bed06bef


После этого можем смело вставлять карточку в raspberry и работать уже на ней, причем у нас осталась резервная копия всей системы на случай каких-то серьезных сбоев.

14 мая 2015 г.

Как проверить zip архив на целостность в Ubuntu

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

Итак, чтобы проверить архив, открываем консоль, переходим в директорию, где лежит наш архив, и запускаем:
zip -T your_firmware.zip
Если архив не битый и с ним все в порядке, то увидим следующее сообщение:
test of your_firmware.zip OK
В этот раз нам повезло и архив в полном порядке, можно обновлять прошивку на смартфоне.