Системные скрипты на php для linux, пишем скриншотер

Многие люди считают что php подходит только для разработки сайтиков, и никак не может быть использован, в других областях применения языков программирования, для создания программ… В этой статье я бы хотел осветить, применение php скриптов «не целевым» образом, а именно мы напишем скрипт который будет делать скрин, выгружать его на yandex диск и выводить адрес скриншота в консоль…

Рассмотрим структуру проекта, она очень проста и состоит из 3-х файлов:

1. screen.php — точка входа в приложение.
2. classes/autoload.php — автолоадер проекта.
3. classes/Request.php — класс реализующий запросы к api яндекса.

Далее расмотрим код screen.php:

Код screen.php
#!/usr/bin/php
<?php
require_once('classis/autoload.php');

$request = new Request();

if(isset($argv[1]) && $argv[1] == '--getToken') {
    echo $request->getOauthLink();die;
}

$home = $_SERVER['HOME'];
$config = include($home . '/.config/scrphp/config.php');
$nameScreenshot = date('Y_m_d_G_i_s_') . 'screen.png';

system('scrot -s /tmp/'.$nameScreenshot);

$result = $request
    ->setToken($config['token'])
    ->setFileNameOnDisk($nameScreenshot)
    ->setPathToFile('/tmp/'.$nameScreenshot)
    ->upload()
    ->publicateFile();

$url = $result['public_url'];

echo $url.PHP_EOL;



Как видите это точка входа в приложение, логика проста:
1. Формирование имени скриншота
2. Вызов системной программы scrot
3. Запрос api yandex.disk и выгрузка скриншота

Файл autoload.php тоже очень прост и состоит всего из трёх строк кода, я приведу его лишь для ознакомления, и мы не будем его рассматривать подробно.

spl_autoload_register(function($name){
    require_once __DIR__.'/'.$name.'.php';
});

Работа с yandex api довольно проста я написал небольшой класс Request.php, с набором неких методов, которые помогают мне в работе с ним…

Листинг Request.php
<?php
class Request
{
    private $_token = null;
    private $_href = null;
    private $_method = null;
    private $_filePath = null;
    private $_fileName = null;

    /**
     * get oauth link
     */
    public function getOauthLink()
    {
        /**
         * https://oauth.yandex.ru/authorize?
         * response_type=token
         * & client_id=<идентификатор приложения>
         * [& device_id=<идентификатор устройства>]
         * [& device_name=<имя устройства>]
         * [& display=popup]
         * [& login_hint=<имя пользователя или электронный адрес>]
         * [& force_confirm=yes]
         * [& state=<произвольная строка>]
         */
        $link = 'https://oauth.yandex.ru/authorize'
            .'?response_type=token'
            . '&client_id=8fc231e60575439fafcdb3b9281778a3';
        echo $link;
    }

    /**
     * set file path on disk
     * @param $filePath
     * @return $this
     */
    public function setFileNameOnDisk($name)
    {
        /**
         * https://cloud-api.yandex.net/v1/disk/resources/upload ?
         * path=<путь, по которому следует загрузить файл>
         */
        $link = 'https://cloud-api.yandex.net/v1/disk/resources/upload?path='.urlencode('/'.trim($name,'/'));
        $response = file_get_contents($link,false,$this->_context('GET'));
        $responseAsArray = json_decode($response,true);
        $this->_href = $responseAsArray['href'];
        $this->_method = $responseAsArray['method'];
        $this->_fileName = $name;
        return $this;
    }

    /**
     * get path to file on local disk
     * @param $path
     * @return $this
     */
    public function setPathToFile($path) {
        $this->_filePath = $path;
        return $this;
    }

    /**
     * upload file to disk
     */
    public function upload()
    {
        $ch = curl_init($this->_href);

        curl_setopt($ch,CURLOPT_HTTPHEADER,
            array(
                'Authorization',
                'OAuth '.$this->_token
            )
        );

        curl_setopt($ch,CURLOPT_INFILE,fopen($this->_filePath,"r"));
        curl_setopt($ch,CURLOPT_INFILESIZE,filesize($this->_filePath));
        curl_setopt($ch,CURLOPT_PUT,true);

        curl_exec($ch);
        curl_close($ch);
        return $this;
    }

    /**
     * public file and get public url for screenshot
     * @return mixed
     */
    public function publicateFile()
    {
        /**
         * https://cloud-api.yandex.net/v1/disk/resources/publish ?
         * path=<путь к публикуемому ресурсу>
         */
        $link = 'https://cloud-api.yandex.net/v1/disk/resources/publish?path='.urlencode('/'.trim($this->_fileName,'/'));
        $response = file_get_contents($link,false,$this->_context('PUT'));
        $responseAsArray = json_decode($response,true);
        $publicateFile = file_get_contents($responseAsArray['href'],false,$this->_context($responseAsArray['method']));
        $publicateFileAsArray = json_decode($publicateFile,true);
        return $publicateFileAsArray;
    }

    /**
     * set oauth token
     * @param $key
     * @return $this
     */
    public function setToken($token)
    {
        $this->_token = $token;
        return $this;
    }

    /**
     * get context for request by file_get_contents
     * @param $method
     * @return resource
     */
    private function _context($method)
    {
        /**
         * Authorization: OAuth <key>
         */
        $opts = array(
            'http'=>array(
                'method'=>$method,
                'header'=>"Authorization: OAuth ".$this->_token."\r\n"
            )
        );

        $context = stream_context_create($opts);
        return $context;
    }

}


Рассмотрим ключевые методы данного класса для запроса методов api в основном я использовал file_get_contents, и так как мне приходилось использовать, его при запросе многих методов я написал метод генерации контекста:

    /**
     * get context for request by file_get_contents
     * @param $method
     * @return resource
     */
    private function _context($method)
    {
        /**
         * Authorization: OAuth <key>
         */
        $opts = array(
            'http'=>array(
                'method'=>$method,
                'header'=>"Authorization: OAuth ".$this->_token."\r\n"
            )
        );

        $context = stream_context_create($opts);
        return $context;
    }

Он тоже довольно прост мы создаём контекст с определённом методом запроса и информации об аутентификации…

Далее нам необходимо «создать файл на yandex.disk» это действие мы производим следующим методом:

  /**
     * set file path on disk
     * @param $filePath
     * @return $this
     */
    public function setFileNameOnDisk($name)
    {
        /**
         * https://cloud-api.yandex.net/v1/disk/resources/upload ?
         * path=<путь, по которому следует загрузить файл>
         */
        $link = 'https://cloud-api.yandex.net/v1/disk/resources/upload?path='.urlencode('/'.trim($name,'/'));
        $response = file_get_contents($link,false,$this->_context('GET'));
        $responseAsArray = json_decode($response,true);
        $this->_href = $responseAsArray['href'];
        $this->_method = $responseAsArray['method'];
        $this->_fileName = $name;
        return $this;
    }

Как я говорил ранее мы запрашиваем api с помощью функции file_get_contents. После того как этот метод отработает, и вся информация будет запрошена, запускаеться метод upload:

    /**
     * upload file to disk
     */
    public function upload()
    {
        $ch = curl_init($this->_href);

        curl_setopt($ch,CURLOPT_HTTPHEADER,
            array(
                'Authorization',
                'OAuth '.$this->_token
            )
        );

        curl_setopt($ch,CURLOPT_INFILE,fopen($this->_filePath,"r"));
        curl_setopt($ch,CURLOPT_INFILESIZE,filesize($this->_filePath));
        curl_setopt($ch,CURLOPT_PUT,true);

        curl_exec($ch);
        curl_close($ch);
        return $this;
    }

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

И так файл загружен, остаёться только опубликовать его и получить прямую ссылку для просмотра, это выполняет функция publicateFile():

    /**
     * public file and get public url for screenshot
     * @return mixed
     */
    public function publicateFile()
    {
        /**
         * https://cloud-api.yandex.net/v1/disk/resources/publish ?
         * path=<путь к публикуемому ресурсу>
         */
        $link = 'https://cloud-api.yandex.net/v1/disk/resources/publish?path='.urlencode('/'.trim($this->_fileName,'/'));
        $response = file_get_contents($link,false,$this->_context('PUT'));
        $responseAsArray = json_decode($response,true);
        $publicateFile = file_get_contents($responseAsArray['href'],false,$this->_context($responseAsArray['method']));
        $publicateFileAsArray = json_decode($publicateFile,true);
        return $publicateFileAsArray;
    }

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

Установка скрипта


Чтож мы закончили, теперь нам предстоит придумать а как же этот скрипт будет работать из консоли? как его запустить там в «глобальной области»? ответом на эти вопросы будет phar — архив содержащий файлы php и способный выполняться как отдельное приложение, похож на тот же jar.

phar мы будем собирать с помощью утилиты box.phar для этого мы пишем простой конфигурационный файл box.json:

{
    "files": [
	"classis/autoload.php",
        "classis/Request.php",
	"screen.php"
    ],
    "main": "screen.php",
    "output": "srcphp.phar",
    "stub": true
}

Для сборки запускаем:

$ php box.phar build

И наш проект готов теперь осталось только установить права на исполнение файла, и скопировать в директорию /usr/bin/srcphp:

$ chmod +x srcphp.phar
$ cp srcphp.phar /usr/bin/srcphp

Не забываем о конфигурации файла /home/myname/.config/srcphp/config.php:

<?php 
// copy this file to /home/user/.config/srcphp/config.php
return array(
    'token' => 'your token'
);

в token необходимо вписать полученный oAuth токен от яндекса, при переходе сгенерированной по средством запуска скрипта с ключом --getToken:

$ srcphp --getToken

Выводы


Главная мысль статьи — рассмотреть как создать консольное приложение с помощью php, на примере программы скриншотера, в следующих я буду поднимать тему использования php в различных сферах применения, и следующая статью будет посвящена разработки простого драйвера usb устройства для linux. Спасибо за внимание и доброго дня.

p.s. Исходный код приложения
Поделиться публикацией
Комментарии 21
    0
    А ещё есть функция gd `imagegrabscreen()`, что позволит избавиться от одного системного вызова.
    Согласен с автором — PHP давно перестал течь, а с 7 версии и производительность подтянули, так что вполне успешно может использоваться вне рамок веба.
      0
      imagegrabscreen только для Windows
        0
        Сейчас протестирую на linux, да она не работает в linux. Но он захватывает весь экран я так понимаю. А scrot даёт выбрать область захвата мышкой.
          0
          Так в манах черным по белому:
          Note:

          This function is only available on Windows
      0
      Класс Request, метод getOauthLink – у Вас в коде зашит client_id. Неплохо бы его вынести в конфигурационный файл.
        –1
        Из начально было так, но из за соображения того что есть лишний какой то пункт в конфиге, который навряд ли кто то будет заполнять, я решил его просто прописать(хотя да некий хардкод, но он не так принципиален).
          0
          По Вашей логике и токен не нужен в конфигурационном файле. Вообще печально, что Вы так думаете…
            0
            Токен как раз в конфигурации находиться, так как он меняется от клиента к клиенту, я опять повторяюсь вынести можно и может даже нужно. Но логика была проста скачав проэкт, пользователь получает токен пишет его в конфиг и копирует конфиг в домашнюю директорию, в другом случае пользователю надо сначала скопировать только потом получить токен, и позже вставить его в конфиг. Что добавляет ещё один не очевидный шаг для инсталяции.
        +1
        Многие люди считают что php подходит только для разработки сайтиков, и никак не может быть использован, в других областях применения языков программирования, для создания программ
        Ну, вообще-то между «подходит» и «может быть использован» огромная пропасть для адекватных людей, стоящих перед выбором инструмента. Любой язык (с несущественными оговорками) можно использовать для решения любой задачи. А вот «подходит» — это отдельный разговор совсем.
          0
          Вы мне только объясните пожалуйста — а почему был выбран именно php? Тем более что для непосредственного снятия скриншота вообще системная утилита вызывается… или вы либу для работы с апи яндекса только под php нашли?
          Я не против php, у меня у самого проект на нём написан, ибо когда начинал, кроме него вообще ничего не мог и не знал, а сейчас переписывать всё это уже не решусь, по этому мне просто интересны аргументы выбора языка, ибо скрипт из 20 строк такой, можно написать на любом ЯП с помощью гугла за 15 минут…
            0
            Можно и какой то подойдёт безусловно лучше чем php. Но в этом то и суть моей статьи что всё таки php можно рассматривать и с точки зрения «системного программирования» (тут я имею ввиду системных скриптов и приложений) а не только как ЯП для создания сайтов.
              +1
              Пользовательские скрипты != системное программирование.
              И да, в данном случае это именно пользовательский скрипт.
                0
                Да замечание верное на счёт терминологии, постараюсь учесть в будущем… Спасибо.
            +5

            Мне вспоминается притча Мастер Фу и десять тысяч строк кода.
            На bash это можно сделать вот так:


            AUTH='USER:PASSWORD'
            FILE=$(date +'%Y_%m_%d_%H_%M_%S_screen.png')
            XML='<propertyupdate xmlns="DAV:"><set><prop><public_url xmlns="urn:yandex:disk:meta">true</public_url></prop></set></propertyupdate>'
            
            scrot -s "/tmp/$FILE"
            curl -s --user "$AUTH" -T "/tmp/$FILE" -X PUT "https://webdav.yandex.ru/"
            curl -s --user "$AUTH" -d "$XML" -X PROPPATCH "https://webdav.yandex.ru/$FILE" | grep -Eo 'https://yadi.sk/[^<]+'
            rm -f "/tmp/$FILE"
              0
              Да и он даже больше для этого подходит. Но цель статьи показать на живом примере, что php можно так же использовать для пользовательских скриптов.
                0

                Конечно, можно. Но зачем? Позвольте, пояснить свою мысль.


                Многие люди считают что php подходит только для разработки сайтиков, и никак не может быть использован, в других областях применения языков программирования, для создания программ…

                Я думаю, те самые "многие люди", сами того не желая, ввели вас в заблуждение, говоря так категорично.
                Имхо, на любом языке программирования можно решить любую задачу, которую обычно решают на другом языке. Язык программирования — это просто набор инструкций, команд, которые нужно выполнить. Естественно, составить инструкцию можно на любом языке.
                Просто обычно смотрят на себестоимость решения задачи на том или ином языке. Под себестоимостью я имею в виду удобство, скорость и простоту написания кода, получившийся объем этого кода. Сюда же можно добавить и скорость выполнения написанного кода.
                На php можно написать что угодно, но во что это обойдется…
                Демон на php может упасть или начать есть память — нужно написать кучу оберток и следить за этим (и перезапускать его). Для драйверов код на php слишком медленный и жрущий память. Для системных скриптов — слишком много кода на единицу функциональности.
                В итоге, если использовать язык вне его ниши, можно больше времени тратить на борьбу с языком и изобретение костылесипедов, чем на создание функциональности. Я думаю, именно это и имели в виду те люди, говоря "никак не может быть использован". Это как "нельзя тыкать шпилькой в розетку" — тыкать-то можно, просто результат вам не понравится.

                  0
                  Плюс шелл-скрипты — набор системных команд, как правило. А PHP-скрипт — лишний слой, которому ещё и интерпретатор нужен (память, время на обработку), перед тем, как запустить системные же команды/приложения. Можно использовать php таким образом, но нет смысла. На вопрос «Зачем?» ответ может быть в данном случае только один — «Потому что можем».
                    0
                    По-моему, для таких системых задач, как создание скриншотов и выгрузки их на Яндекс.Диск — php идеален.
                  0
                  вот я тоже читал и думал: может быть автор круче меня и у него есть какая-то фишка, которая делает пхп круче для решения этой задачи, нежели баш + курл
                  я как бы не программист и стараюсь не вякать в таких темах и послушать умных людей или сам автор может быть умный, ведь все программисты умные и все такое…

                  б-же, никак не избавлюсь от этого комплекса неполноценности, но твой пост дает мне надежду на это
                  оказывается не все программисты умные и наверное я не хуже крутых программистов даже (только почему я не умею писать код?)

                  вобщем если бы мне надо было залить картинку на яндыкс, я бы заюзал баш и курл

                  сейчас мне надо брать гугл календарь за неделю и постить записи в пейсбук
                  для этого у гугла ессть целая пхп библиотека аж на 25 мегабайт и она даже работает

                  для получения списка записей в календаре надо просто сделать гет запрос
                  GET https://www.googleapis.com/calendar/v3/calendars/ид-календаря/events?key={YOUR_API_KEY}
                  получаем

                  200 OK

                  — Show headers —
                  {
                  «kind»: «calendar#events»,
                  «etag»: "\«p338ejguvkehcu0g\»",
                  «summary»: «kalobyte»,
                  «updated»: «2016-09-22T05:50:07.906Z»,
                  «timeZone»: «Europe/Berlin»,
                  «accessRole»: «owner»,
                  «defaultReminders»: [
                  {
                  «method»: «popup»,
                  «minutes»: 30
                  }
                  ],
                  «nextSyncToken»: «CNDpw9-jos8CENDpw9-jos8CGAU=»,
                  «items»: [
                  {

                  «kind»: «calendar#event»,
                  «etag»: "\«2946678667918000\»",
                  «id»: «6uvg9e46eaaenh7hnctojt2d9o»,
                  «status»: «confirmed»,
                  «htmlLink»: «https://www.google.com/calendar/event?eid=NnV2ZzllNDZlYWFlbmg3aG5jdG9qdDJkOW8ga2Fsb2J5dGVAaW5ib3gucnU»,
                  «created»: «2016-09-08T12:55:33.000Z»,
                  «updated»: «2016-09-08T12:55:33.959Z»,
                  «summary»: «aaa»,
                  «creator»: {
                  «email»: "",
                  «self»: true
                  },
                  «organizer»: {
                  «email»: "",
                  «self»: true
                  },
                  «start»: {
                  «dateTime»: «2016-09-08T09:30:00+02:00»
                  },
                  «end»: {
                  «dateTime»: «2016-09-08T15:00:00+02:00»
                  },
                  «iCalUID»: «6uvg9e46eaaenh7hnctojt2d9o@google.com»,
                  «sequence»: 0,
                  «guestsCanInviteOthers»: false,
                  «guestsCanSeeOtherGuests»: false,
                  «reminders»: {
                  «useDefault»: true
                  }
                  }
                  ]
                  }

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

                  на ковыряние и работу я убил 2 недели, потому что мануал библиотеки и примеров не соответствовал апи и вываливалась ошибка 404 и что там не найдено — непонятно и мануал молчит
                  чисто интуитивно поменял ид календаря с primary на конкретный ид и все заработало

                  теперь мне осталось разобраться с апи пейсбука и может быть там тоже есть библиотека пхп
                    0
                    Ну если это GET запрос можно обойтись одной строчкой на php

                    $result = json_decode(file_get_contents('https://www.googleapis.com/calendar/v3/calendars/ид-календаря/events?key={YOUR_API_KEY}'));
                    


                    Но вот на bash это уже сложнее http://stackoverflow.com/questions/1955505/parsing-json-with-unix-tools.
                    0

                    Я к середине статьи тоже задумался о том, мол «блин, да это же можно сделать скриптом из 10 строк на баше, причем тут PHP?».


                    На PHP, в общем-то, можно и GUI-приложения лепить, но зачем?

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

                  Самое читаемое