Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Python predictions processor
19
 *
20
 * @package   mlbackend_python
21
 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace mlbackend_python;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Python predictions processor.
31
 *
32
 * @package   mlbackend_python
33
 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
34
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class processor implements  \core_analytics\classifier, \core_analytics\regressor, \core_analytics\packable {
37
 
38
    /**
39
     * The required version of the python package that performs all calculations.
40
     */
41
    const REQUIRED_PIP_PACKAGE_VERSION = '3.0.5';
42
 
43
    /**
44
     * The python package is installed in a server.
45
     * @var bool
46
     */
47
    protected $useserver;
48
 
49
    /**
50
     * The path to the Python bin.
51
     *
52
     * @var string
53
     */
54
    protected $pathtopython;
55
 
56
    /**
57
     * Remote server host
58
     * @var string
59
     */
60
    protected $host;
61
 
62
    /**
63
     * Remote server port
64
     * @var int
65
     */
66
    protected $port;
67
 
68
    /**
69
     * Whether to use http or https.
70
     * @var bool
71
     */
72
    protected $secure;
73
 
74
    /**
75
     * Server username.
76
     * @var string
77
     */
78
    protected $username;
79
 
80
    /**
81
     * Server password for $this->username.
82
     * @var string
83
     */
84
    protected $password;
85
 
86
    /**
87
     * The constructor.
88
     *
89
     */
90
    public function __construct() {
91
        global $CFG;
92
 
93
        $config = get_config('mlbackend_python');
94
 
95
        $this->useserver = !empty($config->useserver);
96
 
97
        if (!$this->useserver) {
98
            // Set the python location if there is a value.
99
            if (!empty($CFG->pathtopython)) {
100
                $this->pathtopython = $CFG->pathtopython;
101
            }
102
        } else {
103
            $this->host = $config->host ?? '';
104
            $this->port = $config->port ?? '';
105
            $this->secure = $config->secure ?? false;
106
            $this->username = $config->username ?? '';
107
            $this->password = $config->password ?? '';
108
        }
109
    }
110
 
111
    /**
112
     * Is the plugin ready to be used?.
113
     *
114
     * @return bool|string Returns true on success, a string detailing the error otherwise
115
     */
116
    public function is_ready() {
117
 
118
        if (!$this->useserver) {
119
            return $this->is_webserver_ready();
120
        } else {
121
            return $this->is_python_server_ready();
122
        }
123
    }
124
 
125
    /**
126
     * Checks if the python package is available in the web server executing this script.
127
     *
128
     * @return bool|string Returns true on success, a string detailing the error otherwise
129
     */
130
    protected function is_webserver_ready() {
131
        if (empty($this->pathtopython)) {
132
            $settingurl = new \moodle_url('/admin/settings.php', array('section' => 'systempaths'));
133
            return get_string('pythonpathnotdefined', 'mlbackend_python', $settingurl->out());
134
        }
135
 
136
        // Check the installed pip package version.
137
        $cmd = "{$this->pathtopython} -m moodlemlbackend.version";
138
 
139
        $output = null;
140
        $exitcode = null;
141
        // Execute it sending the standard error to $output.
142
        $result = exec($cmd . ' 2>&1', $output, $exitcode);
143
 
144
        if ($exitcode != 0) {
145
            return get_string('pythonpackagenotinstalled', 'mlbackend_python', $cmd);
146
        }
147
 
148
        $vercheck = self::check_pip_package_version($result);
149
        return $this->version_check_return($result, $vercheck);
150
    }
151
 
152
    /**
153
     * Checks if the server can be accessed.
154
     *
155
     * @return bool|string True or an error string.
156
     */
157
    protected function is_python_server_ready() {
158
 
159
        if (empty($this->host) || empty($this->port) || empty($this->username) || empty($this->password)) {
160
            return get_string('errornoconfigdata', 'mlbackend_python');
161
        }
162
 
163
        // Connection is allowed to use 'localhost' and other potentially blocked hosts/ports.
164
        $curl = new \curl(['ignoresecurity' => true]);
165
        $responsebody = $curl->get($this->get_server_url('version')->out(false));
166
        if ($curl->info['http_code'] !== 200) {
167
            return get_string('errorserver', 'mlbackend_python', $this->server_error_str($curl->info['http_code'], $responsebody));
168
        }
169
 
170
        $vercheck = self::check_pip_package_version($responsebody);
171
        return $this->version_check_return($responsebody, $vercheck);
172
 
173
    }
174
 
175
    /**
176
     * Delete the model version output directory.
177
     *
178
     * @throws \moodle_exception
179
     * @param string $uniqueid
180
     * @param string $modelversionoutputdir
181
     * @return null
182
     */
183
    public function clear_model($uniqueid, $modelversionoutputdir) {
184
        if (!$this->useserver) {
185
            remove_dir($modelversionoutputdir);
186
        } else {
187
            // Use the server.
188
 
189
            $url = $this->get_server_url('deletemodel');
190
            list($responsebody, $httpcode) = $this->server_request($url, 'post', ['uniqueid' => $uniqueid]);
191
        }
192
    }
193
 
194
    /**
195
     * Delete the model output directory.
196
     *
197
     * @throws \moodle_exception
198
     * @param string $modeloutputdir
199
     * @param string $uniqueid
200
     * @return null
201
     */
202
    public function delete_output_dir($modeloutputdir, $uniqueid) {
203
        if (!$this->useserver) {
204
            remove_dir($modeloutputdir);
205
        } else {
206
 
207
            $url = $this->get_server_url('deletemodel');
208
            list($responsebody, $httpcode) = $this->server_request($url, 'post', ['uniqueid' => $uniqueid]);
209
        }
210
    }
211
 
212
    /**
213
     * Trains a machine learning algorithm with the provided dataset.
214
     *
215
     * @param string $uniqueid
216
     * @param \stored_file $dataset
217
     * @param string $outputdir
218
     * @return \stdClass
219
     */
220
    public function train_classification($uniqueid, \stored_file $dataset, $outputdir) {
221
 
222
        if (!$this->useserver) {
223
            // Use the local file system.
224
 
225
            list($result, $exitcode) = $this->exec_command('training', [$uniqueid, $outputdir,
226
                $this->get_file_path($dataset)], 'errornopredictresults');
227
 
228
        } else {
229
            // Use the server.
230
 
231
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($outputdir),
232
                'dataset' => $dataset];
233
 
234
            $url = $this->get_server_url('training');
235
            list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
236
        }
237
 
238
        if (!$resultobj = json_decode($result)) {
239
            throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
240
        }
241
 
242
        if ($resultobj->status != 0) {
243
            $resultobj = $this->format_error_info($resultobj);
244
        }
245
 
246
        return $resultobj;
247
    }
248
 
249
    /**
250
     * Classifies the provided dataset samples.
251
     *
252
     * @param string $uniqueid
253
     * @param \stored_file $dataset
254
     * @param string $outputdir
255
     * @return \stdClass
256
     */
257
    public function classify($uniqueid, \stored_file $dataset, $outputdir) {
258
 
259
        if (!$this->useserver) {
260
            // Use the local file system.
261
 
262
            list($result, $exitcode) = $this->exec_command('prediction', [$uniqueid, $outputdir,
263
                $this->get_file_path($dataset)], 'errornopredictresults');
264
 
265
        } else {
266
            // Use the server.
267
 
268
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($outputdir),
269
                'dataset' => $dataset];
270
 
271
            $url = $this->get_server_url('prediction');
272
            list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
273
        }
274
 
275
        if (!$resultobj = json_decode($result)) {
276
            throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
277
        }
278
 
279
        if ($resultobj->status != 0) {
280
            $resultobj = $this->format_error_info($resultobj);
281
        }
282
 
283
        return $resultobj;
284
    }
285
 
286
    /**
287
     * Evaluates this processor classification model using the provided supervised learning dataset.
288
     *
289
     * @param string $uniqueid
290
     * @param float $maxdeviation
291
     * @param int $niterations
292
     * @param \stored_file $dataset
293
     * @param string $outputdir
294
     * @param  string $trainedmodeldir
295
     * @return \stdClass
296
     */
297
    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
298
            $outputdir, $trainedmodeldir) {
299
        global $CFG;
300
 
301
        if (!$this->useserver) {
302
            // Use the local file system.
303
 
304
            $datasetpath = $this->get_file_path($dataset);
305
 
306
            $params = [$uniqueid, $outputdir, $datasetpath, \core_analytics\model::MIN_SCORE,
307
                $maxdeviation, $niterations];
308
 
309
            if ($trainedmodeldir) {
310
                $params[] = $trainedmodeldir;
311
            }
312
 
313
            list($result, $exitcode) = $this->exec_command('evaluation', $params, 'errornopredictresults');
314
            if (!$resultobj = json_decode($result)) {
315
                throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
316
            }
317
 
318
        } else {
319
            // Use the server.
320
 
321
            $requestparams = ['uniqueid' => $uniqueid, 'minscore' => \core_analytics\model::MIN_SCORE,
322
                'maxdeviation' => $maxdeviation, 'niterations' => $niterations,
323
                'dirhash' => $this->hash_dir($outputdir), 'dataset' => $dataset];
324
 
325
            if ($trainedmodeldir) {
326
                $requestparams['trainedmodeldirhash'] = $this->hash_dir($trainedmodeldir);
327
            }
328
 
329
            $url = $this->get_server_url('evaluation');
330
            list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
331
 
332
            if (!$resultobj = json_decode($result)) {
333
                throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
334
            }
335
 
336
            // We need an extra request to get the resources generated during the evaluation process.
337
 
338
            // Directory to temporarly store the evaluation log zip returned by the server.
339
            $evaluationtmpdir = make_request_directory();
340
            $evaluationzippath = $evaluationtmpdir . DIRECTORY_SEPARATOR . 'evaluationlog.zip';
341
 
342
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($outputdir),
343
            'runid' => $resultobj->runid];
344
 
345
            $url = $this->get_server_url('evaluationlog');
346
            list($result, $httpcode) = $this->server_request($url, 'download_one', $requestparams,
347
                ['filepath' => $evaluationzippath]);
348
 
349
            $rundir = $outputdir . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . $resultobj->runid;
350
            if (!mkdir($rundir, $CFG->directorypermissions, true)) {
351
                throw new \moodle_exception('errorexportmodelresult', 'analytics');
352
            }
353
 
354
            $zip = new \zip_packer();
355
            $success = $zip->extract_to_pathname($evaluationzippath, $rundir, null, null, true);
356
            if (!$success) {
357
                $a = 'The evaluation files can not be exported to ' . $rundir;
358
                throw new \moodle_exception('errorpredictionsprocessor', 'analytics', '', $a);
359
            }
360
 
361
            $resultobj->dir = $rundir;
362
        }
363
 
364
        $resultobj = $this->add_extra_result_info($resultobj);
365
 
366
        return $resultobj;
367
    }
368
 
369
    /**
370
     * Exports the machine learning model.
371
     *
372
     * @throws \moodle_exception
373
     * @param  string $uniqueid  The model unique id
374
     * @param  string $modeldir  The directory that contains the trained model.
375
     * @return string            The path to the directory that contains the exported model.
376
     */
377
    public function export(string $uniqueid, string $modeldir): string {
378
 
379
        $exporttmpdir = make_request_directory();
380
 
381
        if (!$this->useserver) {
382
            // Use the local file system.
383
 
384
            // We include an exporttmpdir as we want to be sure that the file is not deleted after the
385
            // python process finishes.
386
            list($exportdir, $exitcode) = $this->exec_command('export', [$uniqueid, $modeldir, $exporttmpdir],
387
                'errorexportmodelresult');
388
 
389
            if ($exitcode != 0) {
390
                throw new \moodle_exception('errorexportmodelresult', 'analytics');
391
            }
392
 
393
        } else {
394
            // Use the server.
395
 
396
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($modeldir)];
397
 
398
            $exportzippath = $exporttmpdir . DIRECTORY_SEPARATOR . 'export.zip';
399
            $url = $this->get_server_url('export');
400
            list($result, $httpcode) = $this->server_request($url, 'download_one', $requestparams,
401
                ['filepath' => $exportzippath]);
402
 
403
            $exportdir = make_request_directory();
404
            $zip = new \zip_packer();
405
            $success = $zip->extract_to_pathname($exportzippath, $exportdir, null, null, true);
406
            if (!$success) {
407
                throw new \moodle_exception('errorexportmodelresult', 'analytics');
408
            }
409
        }
410
 
411
        return $exportdir;
412
    }
413
 
414
    /**
415
     * Imports the provided machine learning model.
416
     *
417
     * @param  string $uniqueid The model unique id
418
     * @param  string $modeldir  The directory that will contain the trained model.
419
     * @param  string $importdir The directory that contains the files to import.
420
     * @return bool Success
421
     */
422
    public function import(string $uniqueid, string $modeldir, string $importdir): bool {
423
 
424
        if (!$this->useserver) {
425
            // Use the local file system.
426
 
427
            list($result, $exitcode) = $this->exec_command('import', [$uniqueid, $modeldir, $importdir],
428
                'errorimportmodelresult');
429
 
430
            if ($exitcode != 0) {
431
                throw new \moodle_exception('errorimportmodelresult', 'analytics');
432
            }
433
 
434
        } else {
435
            // Use the server.
436
 
437
            // Zip the $importdir to send a single file.
438
            $importzipfile = $this->zip_dir($importdir);
439
            if (!$importzipfile) {
440
                // There was an error zipping the directory.
441
                throw new \moodle_exception('errorimportmodelresult', 'analytics');
442
            }
443
 
444
            $requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($modeldir),
445
                'importzip' => curl_file_create($importzipfile, null, 'import.zip')];
446
            $url = $this->get_server_url('import');
447
            list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
448
        }
449
 
450
        return (bool)$result;
451
    }
452
 
453
    /**
454
     * Train this processor regression model using the provided supervised learning dataset.
455
     *
456
     * @throws new \coding_exception
457
     * @param string $uniqueid
458
     * @param \stored_file $dataset
459
     * @param string $outputdir
460
     * @return \stdClass
461
     */
462
    public function train_regression($uniqueid, \stored_file $dataset, $outputdir) {
463
        throw new \coding_exception('This predictor does not support regression yet.');
464
    }
465
 
466
    /**
467
     * Estimates linear values for the provided dataset samples.
468
     *
469
     * @throws new \coding_exception
470
     * @param string $uniqueid
471
     * @param \stored_file $dataset
472
     * @param mixed $outputdir
473
     * @return void
474
     */
475
    public function estimate($uniqueid, \stored_file $dataset, $outputdir) {
476
        throw new \coding_exception('This predictor does not support regression yet.');
477
    }
478
 
479
    /**
480
     * Evaluates this processor regression model using the provided supervised learning dataset.
481
     *
482
     * @throws new \coding_exception
483
     * @param string $uniqueid
484
     * @param float $maxdeviation
485
     * @param int $niterations
486
     * @param \stored_file $dataset
487
     * @param string $outputdir
488
     * @param  string $trainedmodeldir
489
     * @return \stdClass
490
     */
491
    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
492
            $outputdir, $trainedmodeldir) {
493
        throw new \coding_exception('This predictor does not support regression yet.');
494
    }
495
 
496
    /**
497
     * Returns the path to the dataset file.
498
     *
499
     * @param \stored_file $file
500
     * @return string
501
     */
502
    protected function get_file_path(\stored_file $file) {
503
        // From moodle filesystem to the local file system.
504
        // This is not ideal, but there is no read access to moodle filesystem files.
505
        return $file->copy_content_to_temp('core_analytics');
506
    }
507
 
508
    /**
509
     * Check that the given package version can be used and return the error status.
510
     *
511
     * When evaluating the version, we assume the sematic versioning scheme as described at
512
     * https://semver.org/.
513
     *
514
     * @param string $actual The actual Python package version
515
     * @param string $required The required version of the package
516
     * @return int -1 = actual version is too low, 1 = actual version too high, 0 = actual version is ok
517
     */
518
    public static function check_pip_package_version($actual, $required = self::REQUIRED_PIP_PACKAGE_VERSION) {
519
 
520
        if (empty($actual)) {
521
            return -1;
522
        }
523
 
524
        if (version_compare($actual, $required, '<')) {
525
            return -1;
526
        }
527
 
528
        $parts = explode('.', $required);
529
        $requiredapiver = reset($parts);
530
 
531
        $parts = explode('.', $actual);
532
        $actualapiver = reset($parts);
533
 
534
        if ($requiredapiver > 0 || $actualapiver > 1) {
535
            if (version_compare($actual, $requiredapiver + 1, '>=')) {
536
                return 1;
537
            }
538
        }
539
 
540
        return 0;
541
    }
542
 
543
    /**
544
     * Executes the specified module.
545
     *
546
     * @param  string $modulename
547
     * @param  array  $params
548
     * @param  string $errorlangstr
549
     * @return array [0] is the result body and [1] the exit code.
550
     */
551
    protected function exec_command(string $modulename, array $params, string $errorlangstr) {
552
 
553
        $cmd = $this->pathtopython . ' -m moodlemlbackend.' . $modulename . ' ';
554
        foreach ($params as $param) {
555
            $cmd .= escapeshellarg($param) . ' ';
556
        }
557
 
558
        if (!PHPUNIT_TEST && CLI_SCRIPT) {
559
            debugging($cmd, DEBUG_DEVELOPER);
560
        }
561
 
562
        $output = null;
563
        $exitcode = null;
564
        $result = exec($cmd, $output, $exitcode);
565
 
566
        if (!$result) {
567
            throw new \moodle_exception($errorlangstr, 'analytics');
568
        }
569
 
570
        return [$result, $exitcode];
571
    }
572
 
573
    /**
574
     * Formats the errors and info in a single info string.
575
     *
576
     * @param  \stdClass $resultobj
577
     * @return \stdClass
578
     */
579
    private function format_error_info(\stdClass $resultobj) {
580
        if (!empty($resultobj->errors)) {
581
            $errors = $resultobj->errors;
582
            if (is_array($errors)) {
583
                $errors = implode(', ', $errors);
584
            }
585
        } else if (!empty($resultobj->info)) {
586
            // Show info if no errors are returned.
587
            $errors = $resultobj->info;
588
            if (is_array($errors)) {
589
                $errors = implode(', ', $errors);
590
            }
591
        }
592
        $resultobj->info = array(get_string('errorpredictionsprocessor', 'analytics', $errors));
593
 
594
        return $resultobj;
595
    }
596
 
597
    /**
598
     * Returns the url to the python ML server.
599
     *
600
     * @param  string|null $path
601
     * @return \moodle_url
602
     */
603
    private function get_server_url(?string $path = null) {
604
        $protocol = !empty($this->secure) ? 'https' : 'http';
605
        $url = $protocol . '://' . rtrim($this->host, '/');
606
        if (!empty($this->port)) {
607
            $url .= ':' . $this->port;
608
        }
609
 
610
        if ($path) {
611
            $url .= '/' . $path;
612
        }
613
 
614
        return new \moodle_url($url);
615
    }
616
 
617
    /**
618
     * Sends a request to the python ML server.
619
     *
620
     * @param  \moodle_url      $url            The requested url in the python ML server
621
     * @param  string           $method         The curl method to use
622
     * @param  array            $requestparams  Curl request params
623
     * @param  array|null       $options        Curl request options
624
     * @return array                            [0] for the response body and [1] for the http code
625
     */
626
    protected function server_request($url, string $method, array $requestparams, ?array $options = null) {
627
 
628
        if ($method !== 'post' && $method !== 'get' && $method !== 'download_one') {
629
            throw new \coding_exception('Incorrect request method provided. Only "get", "post" and "download_one"
630
                actions are available.');
631
        }
632
 
633
        // Connection is allowed to use 'localhost' and other potentially blocked hosts/ports.
634
        $curl = new \curl(['ignoresecurity' => true]);
635
 
636
        $authorization = $this->username . ':' . $this->password;
637
        $curl->setHeader('Authorization: Basic ' . base64_encode($authorization));
638
 
639
        $responsebody = $curl->{$method}($url, $requestparams, $options);
640
 
641
        if ($curl->info['http_code'] !== 200) {
642
            throw new \moodle_exception('errorserver', 'mlbackend_python', '',
643
                $this->server_error_str($curl->info['http_code'], $responsebody));
644
        }
645
 
646
        return [$responsebody, $curl->info['http_code']];
647
    }
648
 
649
    /**
650
     * Adds extra information to results info.
651
     *
652
     * @param  \stdClass $resultobj
653
     * @return \stdClass
654
     */
655
    protected function add_extra_result_info(\stdClass $resultobj): \stdClass {
656
 
657
        if (!empty($resultobj->dir)) {
658
            $dir = $resultobj->dir . DIRECTORY_SEPARATOR . 'tensor';
659
            $resultobj->info[] = get_string('tensorboardinfo', 'mlbackend_python', $dir);
660
        }
661
        return $resultobj;
662
    }
663
 
664
    /**
665
     * Returns the proper return value for the version checking.
666
     *
667
     * @param  string $actual   Actual moodlemlbackend version
668
     * @param  int    $vercheck Version checking result
669
     * @return true|string      Returns true on success, a string detailing the error otherwise
670
     */
671
    private function version_check_return($actual, $vercheck) {
672
 
673
        if ($vercheck === 0) {
674
            return true;
675
        }
676
 
677
        if ($actual) {
678
            $a = [
679
                'installed' => $actual,
680
                'required' => self::REQUIRED_PIP_PACKAGE_VERSION,
681
            ];
682
 
683
            if ($vercheck < 0) {
684
                return get_string('packageinstalledshouldbe', 'mlbackend_python', $a);
685
 
686
            } else if ($vercheck > 0) {
687
                return get_string('packageinstalledtoohigh', 'mlbackend_python', $a);
688
            }
689
        }
690
 
691
        if (!$this->useserver) {
692
            $cmd = "{$this->pathtopython} -m moodlemlbackend.version";
693
        } else {
694
            // We can't not know which is the python bin in the python ML server, the most likely
695
            // value is 'python'.
696
            $cmd = "python -m moodlemlbackend.version";
697
        }
698
        return get_string('pythonpackagenotinstalled', 'mlbackend_python', $cmd);
699
    }
700
 
701
    /**
702
     * Hashes the provided dir as a string.
703
     *
704
     * @param  string $dir Directory path
705
     * @return string Hash
706
     */
707
    private function hash_dir(string $dir) {
708
        return md5($dir);
709
    }
710
 
711
    /**
712
     * Zips the provided directory.
713
     *
714
     * @param  string $dir Directory path
715
     * @return string The zip filename
716
     */
717
    private function zip_dir(string $dir) {
718
 
719
        $ziptmpdir = make_request_directory();
720
        $ziptmpfile = $ziptmpdir . DIRECTORY_SEPARATOR . 'mlbackend.zip';
721
 
722
        $files = get_directory_list($dir);
723
        $zipfiles = [];
724
        foreach ($files as $file) {
725
            $fullpath = $dir . DIRECTORY_SEPARATOR . $file;
726
            // Use the relative path to the file as the path in the zip.
727
            $zipfiles[$file] = $fullpath;
728
        }
729
 
730
        $zip = new \zip_packer();
731
        if (!$zip->archive_to_pathname($zipfiles, $ziptmpfile)) {
732
            return false;
733
        }
734
 
735
        return $ziptmpfile;
736
    }
737
 
738
    /**
739
     * Error string for httpcode !== 200
740
     *
741
     * @param int       $httpstatuscode The HTTP status code
742
     * @param string    $responsebody   The body of the response
743
     */
744
    private function server_error_str(int $httpstatuscode, string $responsebody): string {
745
        return 'HTTP status code ' . $httpstatuscode . ': ' . $responsebody;
746
    }
747
}