├── .gitignore ├── LICENSE.txt ├── P2p-Cdn.js ├── P2p-Cdn.php ├── README.md ├── examples ├── AudioTest.ogg ├── IMG_20170218_122147549.jpg ├── IMG_20170311_092817396.jpg ├── Torrent.php ├── big.php ├── codice-libero.pdf ├── index.php ├── t.php ├── test.mp4 └── webtorrent.min.js └── namespaced └── P2pcdn └── P2pCdn.php /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/phpstorm 3 | 4 | ### PhpStorm ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff: 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/dictionaries 12 | 13 | # Sensitive or high-churn files: 14 | .idea/**/dataSources/ 15 | .idea/**/dataSources.ids 16 | .idea/**/dataSources.xml 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | 22 | # Gradle: 23 | .idea/**/gradle.xml 24 | .idea/**/libraries 25 | 26 | # CMake 27 | cmake-build-debug/ 28 | 29 | # Mongo Explorer plugin: 30 | .idea/**/mongoSettings.xml 31 | 32 | ## File-based project format: 33 | *.iws 34 | 35 | ## Plugin-specific files: 36 | 37 | # IntelliJ 38 | /out/ 39 | 40 | # mpeltonen/sbt-idea plugin 41 | .idea_modules/ 42 | 43 | # JIRA plugin 44 | atlassian-ide-plugin.xml 45 | 46 | # Cursive Clojure plugin 47 | .idea/replstate.xml 48 | 49 | # Crashlytics plugin (for Android Studio and IntelliJ) 50 | com_crashlytics_export_strings.xml 51 | crashlytics.properties 52 | crashlytics-build.properties 53 | fabric.properties 54 | 55 | ### PhpStorm Patch ### 56 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 57 | 58 | # *.iml 59 | # modules.xml 60 | # .idea/misc.xml 61 | # *.ipr 62 | 63 | # Sonarlint plugin 64 | .idea/sonarlint 65 | 66 | # End of https://www.gitignore.io/api/phpstorm 67 | /examples/ubuntu-17.04-desktop-amd64.iso 68 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright Andrea Paiola, https://andreapaiola.name/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/andreapaiola/P2P-CDN 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. -------------------------------------------------------------------------------- /P2p-Cdn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * P2P-CDN 3 | * ======= 4 | * 5 | * The LAMP hosting is the most popular choice when you want to public on the Web. 6 | * It's cheap and it's fast, but if you want to publish heavy contents like high 7 | * quality photos, audio or videos with many users could become a problem. 8 | * Maybe you want to reduce your hosting bill. 9 | * This is an easy and accessible (lazy load with graceful degradation) fix: 10 | * you can stream all you're heavy files (PDFs included, see examples!) with much 11 | * less bandwidth, sharing the bandwidth of your users. 12 | * Without Javascript or WebRTC all the contents load normally: it's SEO friendly. 13 | * 14 | * This is ECMAScript 6 15 | * https://youtrack.jetbrains.com/issue/WI-31003 16 | * 17 | * You can transpile with Babel https://babeljs.io/ 18 | * 19 | * @author Current authors: Andrea Paiola 20 | * 21 | * @license Code and contributions have 'MIT License' 22 | * More details: https://github.com/andreapaiola/P2P-CDN/blob/master/LICENSE.txt 23 | * 24 | * @link Homepage: https://andreapaiola.name 25 | * Examples: https://github.com/andreapaiola/P2P-CDN/blob/master/examples 26 | * GitHub Repo: https://github.com/andreapaiola/P2P-CDN 27 | */ 28 | 29 | 30 | 'use strict'; 31 | (function (root, p2pCdn) { 32 | 33 | // RequireJS 34 | if (typeof define === 'function' && define.amd) { 35 | define(p2pCdn); 36 | 37 | // CommonJS 38 | } else if (typeof exports === 'object' && typeof module === 'object') { 39 | module.exports = p2pCdn(); 40 | 41 | } else { 42 | root.p2pCdn = p2pCdn(); 43 | } 44 | })(this, function () { 45 | 'use strict'; 46 | 47 | // Do not initialize p2pCdn when running server side, handle it in client: 48 | if (typeof window !== 'object' || (!WebTorrent.WEBRTC_SUPPORT)) return; 49 | 50 | document.addEventListener("DOMContentLoaded", function (event) { 51 | 52 | // Define torrent trackers, if you haven't 53 | if( typeof window.P2PCDNTrackers=="undefined" ){ 54 | var P2PCDNTrackers = [ 55 | 'udp://tracker.openbittorrent.com:80' 56 | ,'udp://tracker.internetwarriors.net:1337' 57 | ,'udp://tracker.leechers-paradise.org:6969' 58 | ,'udp://tracker.coppersurfer.tk:6969' 59 | ,'udp://exodus.desync.com:6969' 60 | ,'wss://tracker.btorrent.xyz' 61 | ,'wss://tracker.openwebtorrent.com' 62 | ,'wss://tracker.fastcast.nz' 63 | ]; 64 | console.log('Default P2PCDNTrackers',P2PCDNTrackers); 65 | } 66 | else{ 67 | var P2PCDNTrackers = window.P2PCDNTrackers; 68 | console.log('Custom P2PCDNTrackers',P2PCDNTrackers); 69 | } 70 | 71 | if( typeof window.P2PCDNEndpoint=="undefined" ){ 72 | const P2PCDNEndpoint = '?file='; 73 | console.log('Default P2PCDNEndpoint',P2PCDNEndpoint); 74 | } 75 | else 76 | { 77 | const P2PCDNEndpoint = window.P2PCDNEndpoint; 78 | console.log('Custom P2PCDNEndpoint',P2PCDNEndpoint); 79 | } 80 | 81 | const client = new WebTorrent(); 82 | const torrents = []; 83 | 84 | const DOMElements = document.getElementsByClassName('p2p-cdn'); 85 | 86 | [].forEach.call(DOMElements, function (el) { 87 | 88 | el.dataset.formatted = el.firstChild.textContent || el.innerHTML; 89 | 90 | if (WebTorrent.WEBRTC_SUPPORT) { 91 | 92 | [].forEach.call(el.dataset.torrents.split(','), function(torrentFileURL) { 93 | if (torrents[torrentFileURL]===undefined) { 94 | torrents[torrentFileURL] = ''; 95 | client.add(torrentFileURL, { 96 | announce: P2PCDNTrackers 97 | }, function (torrent) { 98 | torrent.files[0].getBlobURL(function (err, url) { 99 | if (err) throw err; 100 | torrents[torrentFileURL] = url; 101 | [].forEach.call(DOMElements, function (el) { 102 | const eLTorrents = el.dataset.torrents.split(','); 103 | if( eLTorrents.every(x => torrents[x]!='' ) && el.firstElementChild.tagName.toLowerCase()==='noscript' ) 104 | { 105 | let formatted = el.dataset.formatted; 106 | [].forEach.call(eLTorrents, function (elT) { 107 | console.log(elT); 108 | console.log('P2PCDNEndpoint',P2PCDNEndpoint); 109 | formatted = formatted.replace(new RegExp(elT.replace(P2PCDNEndpoint,''), "g"), torrents[elT]); 110 | }); 111 | el.innerHTML = formatted; 112 | } 113 | }); 114 | }); 115 | 116 | torrent.on('noPeers', function (announceType) { 117 | console.log(announceType); 118 | [].forEach.call(DOMElements, function (el) { 119 | el.innerHTML = formatted; 120 | }); 121 | }); 122 | 123 | torrent.on('done', function () { 124 | console.log('done',torrent.files[0].path); 125 | // https://github.com/webtorrent/webtorrent/issues/1142#issuecomment-312483577 126 | //console.log('infoSpeedDownload',torrent.infoSpeedDownload); 127 | //console.log('infoSpeedUpload',torrent.infoSpeedUpload); 128 | //let average = (array) => array.reduce((a, b) => a + b) / array.length; 129 | //console.log('Average download speed',average(torrent.infoSpeedDownload)); 130 | }); 131 | 132 | torrent.on('download', function (bytes) { 133 | console.log('download','numPeers: '+torrent.numPeers,'downloadSpeed: '+torrent.downloadSpeed+' bytes/sec'); 134 | /* 135 | if( typeof torrent.infoSpeedDownload=='undefined' ){ 136 | torrent.infoSpeedDownload=[]; 137 | } 138 | torrent.infoSpeedDownload.push(torrent.downloadSpeed); 139 | */ 140 | }); 141 | 142 | torrent.on('upload', function (bytes) { 143 | console.log('upload','numPeers: '+torrent.numPeers,'uploadSpeed: '+torrent.uploadSpeed+' bytes/sec'); 144 | /* 145 | if( typeof torrent.infoSpeedUpload=='undefined' ){ 146 | torrent.infoSpeedUpload=[]; 147 | } 148 | torrent.infoSpeedUpload.push(torrent.infoSpeedUpload); 149 | */ 150 | }); 151 | 152 | }); 153 | } 154 | }); 155 | } 156 | else { 157 | el.innerHTML = el.dataset.formatted; 158 | } 159 | }); 160 | }); 161 | }); -------------------------------------------------------------------------------- /P2p-Cdn.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * @license Code and contributions have 'MIT License' 19 | * More details: 20 | * https://github.com/andreapaiola/P2P-CDN/blob/master/LICENSE.txt 21 | * 22 | * @link Homepage: https://andreapaiola.name 23 | * Examples: 24 | * https://github.com/andreapaiola/P2P-CDN/blob/master/examples GitHub Repo: 25 | * https://github.com/andreapaiola/P2P-CDN 26 | */ 27 | class P2p_Cdn { 28 | 29 | function __construct($endpoint='') { 30 | if( empty($endpoint) ) 31 | { 32 | $this->endpoint = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]".'?file='; 33 | } 34 | else 35 | { 36 | $this->endpoint = $endpoint; 37 | } 38 | } 39 | 40 | public function getEndpoint() { 41 | return $this->endpoint; 42 | } 43 | 44 | public function file($filename) { 45 | if (file_exists($filename) && is_file($filename)) { 46 | $file = array( 47 | 'size' => filesize($filename) 48 | ,'lastmod' => filemtime($filename) 49 | ,'filename' => $filename 50 | ,'realpath' => realpath($filename) 51 | ,'basename' => basename($filename) 52 | ,'dirname' => dirname($filename), 53 | ); 54 | return $file; 55 | } 56 | else { 57 | return FALSE; 58 | } 59 | } 60 | 61 | public function render($files, $format) { 62 | $html = ' $value) { 67 | if (is_array($value) && isset($value['filename'])) { 68 | $value = $value['filename']; 69 | } 70 | $format = str_replace('{' . $key . '}', $value, $format); 71 | } 72 | $formatted = $format; 73 | $html .= ' data-torrents="'; 74 | $values = array(); 75 | foreach ($files as $key => $value) { 76 | if (is_array($value) && isset($value['filename'])) { 77 | $value = $value['filename']; 78 | } 79 | $values[] = $this->endpoint . $value; 80 | } 81 | $html .= implode(',', $values); 82 | $html .= '"'; 83 | $html .= '>'; // close p2p-cdn span opening 84 | $html .= ''; 87 | } 88 | else { 89 | return ''; 90 | } 91 | } 92 | else { 93 | return ''; 94 | } 95 | 96 | $html .= ''; 97 | return $html; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [P2P-CDN](https://github.com/andreapaiola/P2P-CDN) — WebTorrent CDN with graceful degradation 2 | ================================================== 3 | 4 | How to use it? 5 | -------------------------------------- 6 | 7 | See examples folder, you can try [here: look that it's an heavy page with images and video (30MB)!](https://andreapaiola.name/P2P-CDN/examples/) 8 | 9 | 10 | Why? 11 | -------------------------------------- 12 | 13 | The LAMP hosting is the most popular choice when you want to public on the Web. 14 | 15 | It's cheap and it's fast, but if you want to publish heavy contents like high quality photos, audio or videos with many 16 | users could become a problem. Maybe you want to reduce your hosting bill. 17 | 18 | This is an easy and accessible ([lazy load with graceful degradation](https://andreapaiola.name/2015-01-13-lazy-load/)) fix: 19 | you can stream all your heavy files (PDFs included, see examples!) with much less bandwidth, sharing the bandwidth 20 | of your users. 21 | 22 | Without Javascript or [WebRTC](http://caniuse.com/#search=webrtc) all the contents load normally: it's SEO friendly. 23 | 24 | Silicon Valley S04E04 - Teambuilding Exercise 25 | 26 | Richard: I read Peter's notes, all of them, and he knew that a peer-to-peer Internet was possible in the future, but he was also basing that on, what, old desktop computers. He never, ever foresaw the ubiquity or power of all of our modern-day smartphones. He was also dealing with a completely different Weissman limit. So... uh, yeah. Yeah. Okay. 27 | 28 | Gavin: What is that? Is that number correct? 29 | 30 | Richard: The delta in mean device efficiency? Yeah, it is. See, eventually, over time, this will render every server that Hooli operates obsolete, which would make Jack Barker shit himself. 31 | 32 | Gavin: Holy shit! 33 | 34 | ![alt text](https://cdn-images-1.medium.com/max/1920/1*iRwNkDy1d8JiR4LRWEDwOw.png "This explain everything") 35 | 36 | 37 | 38 | What is WebTorrent? 39 | -------------------------------------- 40 | 41 | BitTorrent over WebRTC (data channels) to stream in the browser. 42 | 43 | See [WebTorrentFAQ](https://webtorrent.io/faq) and [WebTorrent@GitHub](https://github.com/webtorrent/webtorrent). 44 | 45 | 46 | Generate the torrent with PHP 47 | -------------------------------------- 48 | 49 | [Torrent.php used in examples by Adrien Gibrat](https://github.com/adriengibrat/torrent-rw/blob/master/Torrent.php) 50 | or... ? 51 | 52 | Submit your suggestion! 53 | 54 | 55 | Torrent announce trackers 56 | -------------------------------------- 57 | 58 | Default torrent announce/trackers: 59 | 60 | udp://tracker.openbittorrent.com:80 61 | udp://tracker.internetwarriors.net:1337 62 | udp://tracker.leechers-paradise.org:6969 63 | udp://tracker.coppersurfer.tk:6969 64 | udp://exodus.desync.com:6969 65 | wss://tracker.btorrent.xyz 66 | wss://tracker.openwebtorrent.com 67 | wss://tracker.fastcast.nz 68 | 69 | You can setup a private tracker with [bittorrent-tracker](https://github.com/webtorrent/bittorrent-tracker) or 70 | what you prefer. 71 | 72 | Apache notes 73 | -------------------------------------- 74 | 75 | Disabled gzip compression for .mp4 files with 76 | 77 | SetOutputFilter DEFLATE 78 | 79 | SetEnvIfNoCase Request_URI \.mp4$ no-gzip dont-vary 80 | 81 | in the .htaccess if you see [this error](https://github.com/webtorrent/webtorrent/issues/1080) 82 | 83 | TODO - wish list 84 | -------------------------------------- 85 | 86 | Configurable torrent trackers - done 87 | 88 | Configurable torrent endpoint - done 89 | 90 | WebRTC Peer-to-peer in Safari 11 (iOS 11) Works 91 | 92 | composer.json 93 | 94 | documentation 95 | 96 | check/enforce Drupal 8 / Symfony 2 coding standard 97 | https://www.drupal.org/docs/develop/development-tools/configuring-phpstorm 98 | https://www.drupal.org/docs/develop/standards/coding-standards 99 | https://confluence.jetbrains.com/display/PhpStorm/Drupal+Development+using+PhpStorm#DrupalDevelopmentusingPhpStorm-CoderandPHPCodeSnifferIntegration 100 | 101 | Define and expand compatibility 102 | 103 | Explore platforms other than LAMP, maybe Node.js? 104 | [webtorrent-hybrid](https://github.com/webtorrent/webtorrent-hybrid) 105 | 106 | Tests (PHPUnit? Karma? Jasmine?) 107 | 108 | Integration with popular frameworks and CMS like Laravel, Symfony, Wordpress and Drupal, plugins and modules 109 | 110 | A logo maybe? :P 111 | 112 | 113 | ### License 114 | 115 | MIT. Copyright (c) [Andrea Paiola](https://andreapaiola.name). 116 | -------------------------------------------------------------------------------- /examples/AudioTest.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreapaiola/P2P-CDN/04c45051e77247d6769a6e80095a3c56a46fb046/examples/AudioTest.ogg -------------------------------------------------------------------------------- /examples/IMG_20170218_122147549.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreapaiola/P2P-CDN/04c45051e77247d6769a6e80095a3c56a46fb046/examples/IMG_20170218_122147549.jpg -------------------------------------------------------------------------------- /examples/IMG_20170311_092817396.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreapaiola/P2P-CDN/04c45051e77247d6769a6e80095a3c56a46fb046/examples/IMG_20170311_092817396.jpg -------------------------------------------------------------------------------- /examples/Torrent.php: -------------------------------------------------------------------------------- 1 | 21 | require_once 'Torrent.php'; 22 | 23 | // get torrent infos 24 | $torrent = new Torrent( './test.torrent' ); 25 | echo '
private: ', $torrent->is_private() ? 'yes' : 'no', 26 | '
announce: ', $torrent->announce(), 27 | '
name: ', $torrent->name(), 28 | '
comment: ', $torrent->comment(), 29 | '
piece_length: ', $torrent->piece_length(), 30 | '
size: ', $torrent->size( 2 ), 31 | '
hash info: ', $torrent->hash_info(), 32 | '
stats: '; 33 | var_dump( $torrent->scrape() ); 34 | echo '
content: '; 35 | var_dump( $torrent->content() ); 36 | echo '
source: ', 37 | $torrent; 38 | 39 | // get magnet link 40 | $torrent->magnet(); // use $torrent->magnet( false ); to get non html encoded ampersand 41 | 42 | // create torrent 43 | $torrent = new Torrent( array( 'test.mp3', 'test.jpg' ), 'http://torrent.tracker/annonce' ); 44 | $torrent->save('test.torrent'); // save to disk 45 | 46 | // modify torrent 47 | $torrent->announce('http://alternate-torrent.tracker/annonce'); // add a tracker 48 | $torrent->announce(false); // reset announce trackers 49 | $torrent->announce(array('http://torrent.tracker/annonce', 'http://alternate-torrent.tracker/annonce')); // set tracker(s), it also works with a 'one tracker' array... 50 | $torrent->announce(array(array('http://torrent.tracker/annonce', 'http://alternate-torrent.tracker/annonce'), 'http://another-torrent.tracker/annonce')); // set tiered trackers 51 | $torrent->comment('hello world'); 52 | $torrent->name('test torrent'); 53 | $torrent->is_private(true); 54 | $torrent->httpseeds('http://file-hosting.domain/path/'); // BitTornado implementation 55 | $torrent->url_list(array('http://file-hosting.domain/path/','http://another-file-hosting.domain/path/')); // 56 | GetRight implementation 57 | 58 | // print errors 59 | if ( $errors = $torrent->errors() ) 60 | var_dump( $errors ); 61 | 62 | // send to user 63 | $torrent->send(); 64 | * 65 | * 66 | * @author Adrien Gibrat 67 | * @tester Jeong, Anton, dokcharlie, official testers ;) Thanks for your precious feedback 68 | * @copyleft 2010 - Just use it! 69 | * @license http://www.gnu.org/licenses/gpl.html GNU General Public License version 3 70 | * @version 0.0.3 71 | */ 72 | 73 | class Torrent { 74 | 75 | /** 76 | * @const float Default http timeout 77 | */ 78 | const timeout = 30; 79 | 80 | /** 81 | * @var array List of error occurred 82 | */ 83 | static protected $_errors = array(); 84 | 85 | /** Read and decode torrent file/data OR build a torrent from source folder/file(s) 86 | * Supported signatures: 87 | * - Torrent(); // get an instance (useful to scrape and check errors) 88 | * - Torrent( string $torrent ); // analyze a torrent file 89 | * - Torrent( string $torrent, string $announce ); 90 | * - Torrent( string $torrent, array $meta ); 91 | * - Torrent( string $file_or_folder ); // create a torrent file 92 | * - Torrent( string $file_or_folder, string $announce_url, [int $piece_length] ); 93 | * - Torrent( string $file_or_folder, array $meta, [int $piece_length] ); 94 | * - Torrent( array $files_list ); 95 | * - Torrent( array $files_list, string $announce_url, [int $piece_length] ); 96 | * - Torrent( array $files_list, array $meta, [int $piece_length] ); 97 | * @param string|array torrent to read or source folder/file(s) (optional, to get an instance) 98 | * @param string|array announce url or meta informations (optional) 99 | * @param int piece length (optional) 100 | */ 101 | public function __construct ( $data = null, $meta = array(), $piece_length = 256 ) { 102 | if ( is_null( $data ) ) 103 | return false; 104 | if ( $piece_length < 32 || $piece_length > 4096 ) 105 | return self::set_error( new Exception( 'Invalid piece length, must be between 32 and 4096' ) ); 106 | if ( is_string( $meta ) ) 107 | $meta = array( 'announce' => $meta ); 108 | if ( $this->build( $data, $piece_length * 1024 ) ) 109 | $this->touch(); 110 | else 111 | $meta = array_merge( $meta, $this->decode( $data ) ); 112 | foreach( $meta as $key => $value ) 113 | $this->{$key} = $value; 114 | } 115 | 116 | /** Convert the current Torrent instance in torrent format 117 | * @return string encoded torrent data 118 | */ 119 | public function __toString() { 120 | return $this->encode( $this ); 121 | } 122 | 123 | /** Return last error message 124 | * @return string|boolean last error message or false if none 125 | */ 126 | public function error() { 127 | return empty( self::$_errors ) ? 128 | false : 129 | self::$_errors[0]->getMessage(); 130 | } 131 | 132 | /** Return Errors 133 | * @return array|boolean error list or false if none 134 | */ 135 | public function errors() { 136 | return empty( self::$_errors ) ? 137 | false : 138 | self::$_errors; 139 | } 140 | 141 | /**** Getters and setters ****/ 142 | 143 | /** Getter and setter of torrent announce url / list 144 | * If the argument is a string, announce url is added to announce list (or set as announce if announce is not set) 145 | * If the argument is an array/object, set announce url (with first url) and list (if array has more than one url), tiered list supported 146 | * If the argument is false announce url & list are unset 147 | * @param null|false|string|array announce url / list, reset all if false (optional, if omitted it's a getter) 148 | * @return string|array|null announce url / list or null if not set 149 | */ 150 | public function announce ( $announce = null ) { 151 | if ( is_null( $announce ) ) 152 | return ! isset( $this->{'announce-list'} ) ? 153 | isset( $this->announce ) ? $this->announce : null : 154 | $this->{'announce-list'}; 155 | $this->touch(); 156 | if ( is_string( $announce ) && isset( $this->announce ) ) 157 | return $this->{'announce-list'} = self::announce_list( isset( $this->{'announce-list'} ) ? $this->{'announce-list'} : $this->announce, $announce ); 158 | unset( $this->{'announce-list'} ); 159 | if ( is_array( $announce ) || is_object( $announce ) ) 160 | if ( ( $this->announce = self::first_announce( $announce ) ) && count( $announce ) > 1 ) 161 | return $this->{'announce-list'} = self::announce_list( $announce ); 162 | else 163 | return $this->announce; 164 | if ( ! isset( $this->announce ) && $announce ) 165 | return $this->announce = (string) $announce; 166 | unset( $this->announce ); 167 | } 168 | 169 | /** Getter and setter of torrent creation date 170 | * @param null|integer timestamp (optional, if omitted it's a getter) 171 | * @return integer|null timestamp or null if not set 172 | */ 173 | public function creation_date ( $timestamp = null ) { 174 | return is_null( $timestamp ) ? 175 | isset( $this->{'creation date'} ) ? $this->{'creation date'} : null : 176 | $this->touch( $this->{'creation date'} = (int) $timestamp ); 177 | } 178 | 179 | /** Getter and setter of torrent comment 180 | * @param null|string comment (optional, if omitted it's a getter) 181 | * @return string|null comment or null if not set 182 | */ 183 | public function comment ( $comment = null ) { 184 | return is_null( $comment ) ? 185 | isset( $this->comment ) ? $this->comment : null : 186 | $this->touch( $this->comment = (string) $comment ); 187 | } 188 | 189 | /** Getter and setter of torrent name 190 | * @param null|string name (optional, if omitted it's a getter) 191 | * @return string|null name or null if not set 192 | */ 193 | public function name ( $name = null ) { 194 | return is_null( $name ) ? 195 | isset( $this->info['name'] ) ? $this->info['name'] : null : 196 | $this->touch( $this->info['name'] = (string) $name ); 197 | } 198 | 199 | /** Getter and setter of private flag 200 | * @param null|boolean is private or not (optional, if omitted it's a getter) 201 | * @return boolean private flag 202 | */ 203 | public function is_private ( $private = null ) { 204 | return is_null( $private ) ? 205 | ! empty( $this->info['private'] ) : 206 | $this->touch( $this->info['private'] = $private ? 1 : 0 ); 207 | } 208 | 209 | /** Getter and setter of torrent source 210 | * @param null|string source (optional, if omitted it's a getter) 211 | * @return string|null source or null if not set 212 | */ 213 | public function source ( $source = null ) { 214 | return is_null( $source ) ? 215 | isset( $this->info['source'] ) ? $this->info['source'] : null : 216 | $this->touch( $this->info['source'] = (string) $source ); 217 | 218 | } 219 | 220 | /** Getter and setter of webseed(s) url list ( GetRight implementation ) 221 | * @param null|string|array webseed or webseeds mirror list (optional, if omitted it's a getter) 222 | * @return string|array|null webseed(s) or null if not set 223 | */ 224 | public function url_list ( $urls = null ) { 225 | return is_null( $urls ) ? 226 | isset( $this->{'url-list'} ) ? $this->{'url-list'} : null : 227 | $this->touch( $this->{'url-list'} = is_string( $urls) ? $urls : (array) $urls ); 228 | } 229 | 230 | /** Getter and setter of httpseed(s) url list ( BitTornado implementation ) 231 | * @param null|string|array httpseed or httpseeds mirror list (optional, if omitted it's a getter) 232 | * @return array|null httpseed(s) or null if not set 233 | */ 234 | public function httpseeds ( $urls = null ) { 235 | return is_null( $urls ) ? 236 | isset( $this->httpseeds ) ? $this->httpseeds : null : 237 | $this->touch( $this->httpseeds = (array) $urls ); 238 | } 239 | 240 | /**** Analyze BitTorrent ****/ 241 | 242 | /** Get piece length 243 | * @return integer piece length or null if not set 244 | */ 245 | public function piece_length () { 246 | return isset( $this->info['piece length'] ) ? 247 | $this->info['piece length'] : 248 | null; 249 | } 250 | 251 | /** Compute hash info 252 | * @return string hash info or null if info not set 253 | */ 254 | public function hash_info () { 255 | return isset( $this->info ) ? 256 | sha1( self::encode( $this->info ) ) : 257 | null; 258 | } 259 | 260 | /** List torrent content 261 | * @param integer|null size precision (optional, if omitted returns sizes in bytes) 262 | * @return array file(s) and size(s) list, files as keys and sizes as values 263 | */ 264 | public function content ( $precision = null ) { 265 | $files = array(); 266 | if ( isset( $this->info['files'] ) && is_array( $this->info['files'] ) ) 267 | foreach ( $this->info['files'] as $file ) 268 | $files[self::path( $file['path'], $this->info['name'] )] = $precision ? 269 | self::format( $file['length'], $precision ) : 270 | $file['length']; 271 | elseif ( isset( $this->info['name'] ) ) 272 | $files[$this->info['name']] = $precision ? 273 | self::format( $this->info['length'], $precision ) : 274 | $this->info['length']; 275 | return $files; 276 | } 277 | 278 | /** List torrent content pieces and offset(s) 279 | * @return array file(s) and pieces/offset(s) list, file(s) as keys and pieces/offset(s) as values 280 | */ 281 | public function offset () { 282 | $files = array(); 283 | $size = 0; 284 | if ( isset( $this->info['files'] ) && is_array( $this->info['files'] ) ) 285 | foreach ( $this->info['files'] as $file ) 286 | $files[self::path( $file['path'], $this->info['name'] )] = array( 287 | 'startpiece' => floor( $size / $this->info['piece length'] ), 288 | 'offset' => fmod( $size, $this->info['piece length'] ), 289 | 'size' => $size += $file['length'], 290 | 'endpiece' => floor( $size / $this->info['piece length'] ) 291 | ); 292 | elseif ( isset( $this->info['name'] ) ) 293 | $files[$this->info['name']] = array( 294 | 'startpiece' => 0, 295 | 'offset' => 0, 296 | 'size' => $this->info['length'], 297 | 'endpiece' => floor( $this->info['length'] / $this->info['piece length'] ) 298 | ); 299 | return $files; 300 | } 301 | 302 | /** Sum torrent content size 303 | * @param integer|null size precision (optional, if omitted returns size in bytes) 304 | * @return integer|string file(s) size 305 | */ 306 | public function size ( $precision = null ) { 307 | $size = 0; 308 | if ( isset( $this->info['files'] ) && is_array( $this->info['files'] ) ) 309 | foreach ( $this->info['files'] as $file ) 310 | $size += $file['length']; 311 | elseif ( isset( $this->info['name'] ) ) 312 | $size = $this->info['length']; 313 | return is_null( $precision ) ? 314 | $size : 315 | self::format( $size, $precision ); 316 | } 317 | 318 | /** Request torrent statistics from scrape page USING CURL!! 319 | * @param string|array announce or scrape page url (optional, to request an alternative tracker BUT required for static call) 320 | * @param string torrent hash info (optional, required ONLY for static call) 321 | * @param float read timeout in seconds (optional, default to self::timeout 30s) 322 | * @return array tracker torrent statistics 323 | */ 324 | /* static */ public function scrape ( $announce = null, $hash_info = null, $timeout = self::timeout ) { 325 | $packed_hash = urlencode( pack('H*', $hash_info ? $hash_info : $this->hash_info() ) ); 326 | $handles = $scrape = array(); 327 | if ( ! function_exists( 'curl_multi_init' ) ) 328 | return self::set_error( new Exception( 'Install CURL with "curl_multi_init" enabled' ) ); 329 | $curl = curl_multi_init(); 330 | foreach ( (array) ($announce ? $announce : $this->announce()) as $tier ) 331 | foreach ( (array) $tier as $tracker ) { 332 | $tracker = str_ireplace( array( 'udp://', '/announce', ':80/' ), array( 'http://', '/scrape', '/' ), $tracker ); 333 | if ( isset( $handles[$tracker] ) ) 334 | continue; 335 | $handles[$tracker] = curl_init( $tracker . '?info_hash=' . $packed_hash ); 336 | curl_setopt( $handles[$tracker], CURLOPT_RETURNTRANSFER, true ); 337 | curl_setopt( $handles[$tracker], CURLOPT_TIMEOUT, $timeout ); 338 | curl_multi_add_handle( $curl, $handles[$tracker] ); 339 | } 340 | do { 341 | while ( ( $state = curl_multi_exec( $curl, $running ) ) == CURLM_CALL_MULTI_PERFORM ); 342 | if( $state != CURLM_OK ) 343 | continue; 344 | while ( $done = curl_multi_info_read( $curl ) ) { 345 | $info = curl_getinfo( $done['handle'] ); 346 | $tracker = explode( '?', $info['url'], 2 ); 347 | $tracker = array_shift( $tracker ); 348 | if ( empty( $info['http_code'] ) ) { 349 | $scrape[$tracker] = self::set_error( new Exception( 'Tracker request timeout (' . $timeout . 's)' ), true ); 350 | continue; 351 | } elseif ( $info['http_code'] != 200 ) { 352 | $scrape[$tracker] = self::set_error( new Exception( 'Tracker request failed (' . $info['http_code'] . ' code)' ), true ); 353 | continue; 354 | } 355 | $data = curl_multi_getcontent( $done['handle'] ); 356 | $stats = self::decode_data( $data ); 357 | curl_multi_remove_handle( $curl, $done['handle'] ); 358 | $scrape[$tracker] = empty( $stats['files'] ) ? 359 | self::set_error( new Exception( 'Empty scrape data' ), true ) : 360 | array_shift( $stats['files'] ) + ( empty( $stats['flags'] ) ? array() : $stats['flags'] ); 361 | } 362 | } while ( $running ); 363 | curl_multi_close( $curl ); 364 | return $scrape; 365 | } 366 | 367 | /**** Save and Send ****/ 368 | 369 | /** Save torrent file to disk 370 | * @param null|string name of the file (optional) 371 | * @return boolean file has been saved or not 372 | */ 373 | public function save ( $filename = null ) { 374 | return file_put_contents( is_null( $filename ) ? $this->info['name'] . '.torrent' : $filename, $this->encode( $this ) ); 375 | } 376 | 377 | /** Send torrent file to client 378 | * @param null|string name of the file (optional) 379 | * @return void script exit 380 | */ 381 | public function send ( $filename = null ) { 382 | $data = $this->encode( $this ); 383 | header( 'Content-type: application/x-bittorrent' ); 384 | header( 'Content-Length: ' . strlen( $data ) ); 385 | header( 'Content-Disposition: attachment; filename="' . ( is_null( $filename ) ? $this->info['name'] . '.torrent' : $filename ) . '"' ); 386 | exit( $data ); 387 | } 388 | 389 | /** Get magnet link 390 | * @param boolean html encode ampersand, default true (optional) 391 | * @return string magnet link 392 | */ 393 | public function magnet ( $html = true ) { 394 | $ampersand = $html ? '&' : '&'; 395 | return sprintf( 'magnet:?xt=urn:btih:%2$s%1$sdn=%3$s%1$sxl=%4$d%1$str=%5$s', $ampersand, $this->hash_info(), urlencode( $this->name() ), $this->size(), implode( $ampersand .'tr=', self::untier( $this->announce() ) ) ); 396 | } 397 | 398 | /**** Encode BitTorrent ****/ 399 | 400 | /** Encode torrent data 401 | * @param mixed data to encode 402 | * @return string torrent encoded data 403 | */ 404 | static public function encode ( $mixed ) { 405 | switch ( gettype( $mixed ) ) { 406 | case 'integer': 407 | case 'double': 408 | return self::encode_integer( $mixed ); 409 | case 'object': 410 | $mixed = get_object_vars( $mixed ); 411 | case 'array': 412 | return self::encode_array( $mixed ); 413 | default: 414 | return self::encode_string( (string) $mixed ); 415 | } 416 | } 417 | 418 | /** Encode torrent string 419 | * @param string string to encode 420 | * @return string encoded string 421 | */ 422 | static private function encode_string ( $string ) { 423 | return strlen( $string ) . ':' . $string; 424 | } 425 | 426 | /** Encode torrent integer 427 | * @param integer integer to encode 428 | * @return string encoded integer 429 | */ 430 | static private function encode_integer ( $integer ) { 431 | return 'i' . $integer . 'e'; 432 | } 433 | 434 | /** Encode torrent dictionary or list 435 | * @param array array to encode 436 | * @return string encoded dictionary or list 437 | */ 438 | static private function encode_array ( $array ) { 439 | if ( self::is_list( $array ) ) { 440 | $return = 'l'; 441 | foreach ( $array as $value ) 442 | $return .= self::encode( $value ); 443 | } else { 444 | ksort( $array, SORT_STRING ); 445 | $return = 'd'; 446 | foreach ( $array as $key => $value ) 447 | $return .= self::encode( strval( $key ) ) . self::encode( $value ); 448 | } 449 | return $return . 'e'; 450 | } 451 | 452 | /**** Decode BitTorrent ****/ 453 | 454 | /** Decode torrent data or file 455 | * @param string data or file path to decode 456 | * @return array decoded torrent data 457 | */ 458 | static protected function decode ( $string ) { 459 | $data = is_file( $string ) || self::url_exists( $string ) ? 460 | self::file_get_contents( $string ) : 461 | $string; 462 | return (array) self::decode_data( $data ); 463 | } 464 | 465 | /** Decode torrent data 466 | * @param string data to decode 467 | * @return array decoded torrent data 468 | */ 469 | static private function decode_data ( & $data ) { 470 | switch( self::char( $data ) ) { 471 | case 'i': 472 | $data = substr( $data, 1 ); 473 | return self::decode_integer( $data ); 474 | case 'l': 475 | $data = substr( $data, 1 ); 476 | return self::decode_list( $data ); 477 | case 'd': 478 | $data = substr( $data, 1 ); 479 | return self::decode_dictionary( $data ); 480 | default: 481 | return self::decode_string( $data ); 482 | } 483 | } 484 | 485 | /** Decode torrent dictionary 486 | * @param string data to decode 487 | * @return array decoded dictionary 488 | */ 489 | static private function decode_dictionary ( & $data ) { 490 | $dictionary = array(); 491 | $previous = null; 492 | while ( ( $char = self::char( $data ) ) != 'e' ) { 493 | if ( $char === false ) 494 | return self::set_error( new Exception( 'Unterminated dictionary' ) ); 495 | if ( ! ctype_digit( $char ) ) 496 | return self::set_error( new Exception( 'Invalid dictionary key' ) ); 497 | $key = self::decode_string( $data ); 498 | if ( isset( $dictionary[$key] ) ) 499 | return self::set_error( new Exception( 'Duplicate dictionary key' ) ); 500 | if ( $key < $previous ) 501 | self::set_error( new Exception( 'Missorted dictionary key' ) ); 502 | $dictionary[$key] = self::decode_data( $data ); 503 | $previous = $key; 504 | } 505 | $data = substr( $data, 1 ); 506 | return $dictionary; 507 | } 508 | 509 | /** Decode torrent list 510 | * @param string data to decode 511 | * @return array decoded list 512 | */ 513 | static private function decode_list ( & $data ) { 514 | $list = array(); 515 | while ( ( $char = self::char( $data ) ) != 'e' ) { 516 | if ( $char === false ) 517 | return self::set_error( new Exception( 'Unterminated list' ) ); 518 | $list[] = self::decode_data( $data ); 519 | } 520 | $data = substr( $data, 1 ); 521 | return $list; 522 | } 523 | 524 | /** Decode torrent string 525 | * @param string data to decode 526 | * @return string decoded string 527 | */ 528 | static private function decode_string ( & $data ) { 529 | if ( self::char( $data ) === '0' && substr( $data, 1, 1 ) != ':' ) 530 | self::set_error( new Exception( 'Invalid string length, leading zero' ) ); 531 | if ( ! $colon = @strpos( $data, ':' ) ) 532 | return self::set_error( new Exception( 'Invalid string length, colon not found' ) ); 533 | $length = intval( substr( $data, 0, $colon ) ); 534 | if ( $length + $colon + 1 > strlen( $data ) ) 535 | return self::set_error( new Exception( 'Invalid string, input too short for string length' ) ); 536 | $string = substr( $data, $colon + 1, $length ); 537 | $data = substr( $data, $colon + $length + 1 ); 538 | return $string; 539 | } 540 | 541 | /** Decode torrent integer 542 | * @param string data to decode 543 | * @return integer decoded integer 544 | */ 545 | static private function decode_integer ( & $data ) { 546 | $start = 0; 547 | $end = strpos( $data, 'e'); 548 | if ( $end === 0 ) 549 | self::set_error( new Exception( 'Empty integer' ) ); 550 | if ( self::char( $data ) == '-' ) 551 | $start++; 552 | if ( substr( $data, $start, 1 ) == '0' && $end > $start + 1 ) 553 | self::set_error( new Exception( 'Leading zero in integer' ) ); 554 | if ( ! ctype_digit( substr( $data, $start, $start ? $end - 1 : $end ) ) ) 555 | self::set_error( new Exception( 'Non-digit characters in integer' ) ); 556 | $integer = substr( $data, 0, $end ); 557 | $data = substr( $data, $end + 1 ); 558 | return 0 + $integer; 559 | } 560 | 561 | /**** Internal Helpers ****/ 562 | 563 | /** Build torrent info 564 | * @param string|array source folder/file(s) path 565 | * @param integer piece length 566 | * @return array|boolean torrent info or false if data isn't folder/file(s) 567 | */ 568 | protected function build ( $data, $piece_length ) { 569 | if ( is_null( $data ) ) 570 | return false; 571 | elseif ( is_array( $data ) && self::is_list( $data ) ) 572 | return $this->info = $this->files( $data, $piece_length ); 573 | elseif ( is_dir( $data ) ) 574 | return $this->info = $this->folder( $data, $piece_length ); 575 | elseif ( ( is_file( $data ) || self::url_exists( $data ) ) && ! self::is_torrent( $data ) ) 576 | return $this->info = $this->file( $data, $piece_length ); 577 | else 578 | return false; 579 | } 580 | 581 | /** Set torrent creator and creation date 582 | * @param any param 583 | * @return any param 584 | */ 585 | protected function touch ( $void = null ) { 586 | $this->{'created by'} = 'Torrent RW PHP Class - http://github.com/adriengibrat/torrent-rw'; 587 | $this->{'creation date'} = time(); 588 | return $void; 589 | } 590 | 591 | /** Add an error to errors stack 592 | * @param Exception error to add 593 | * @param boolean return error message or not (optional, default to false) 594 | * @return boolean|string return false or error message if requested 595 | */ 596 | static protected function set_error ( $exception, $message = false ) { 597 | return ( array_unshift( self::$_errors, $exception ) && $message ) ? $exception->getMessage() : false; 598 | } 599 | 600 | /** Build announce list 601 | * @param string|array announce url / list 602 | * @param string|array announce url / list to add (optionnal) 603 | * @return array announce list (array of arrays) 604 | */ 605 | static protected function announce_list( $announce, $merge = array() ) { 606 | return array_map( create_function( '$a', 'return (array) $a;' ), array_merge( (array) $announce, (array) $merge ) ); 607 | } 608 | 609 | /** Get the first announce url in a list 610 | * @param array announce list (array of arrays if tiered trackers) 611 | * @return string first announce url 612 | */ 613 | static protected function first_announce( $announce ) { 614 | while ( is_array( $announce ) ) 615 | $announce = reset( $announce ); 616 | return $announce; 617 | } 618 | 619 | /** Helper to pack data hash 620 | * @param string data 621 | * @return string packed data hash 622 | */ 623 | static protected function pack ( & $data ) { 624 | return pack('H*', sha1( $data ) ) . ( $data = null ); 625 | } 626 | 627 | /** Helper to build file path 628 | * @param array file path 629 | * @param string base folder 630 | * @return string real file path 631 | */ 632 | static protected function path ( $path, $folder ) { 633 | array_unshift( $path, $folder ); 634 | return join( DIRECTORY_SEPARATOR, $path ); 635 | } 636 | 637 | /** Helper to explode file path 638 | * @param string file path 639 | * @return array file path 640 | */ 641 | static protected function path_explode ( $path ) { 642 | return explode( DIRECTORY_SEPARATOR, $path ); 643 | } 644 | 645 | /** Helper to test if an array is a list 646 | * @param array array to test 647 | * @return boolean is the array a list or not 648 | */ 649 | static protected function is_list ( $array ) { 650 | foreach ( array_keys( $array ) as $key ) 651 | if ( ! is_int( $key ) ) 652 | return false; 653 | return true; 654 | } 655 | 656 | /** Build pieces depending on piece length from a file handler 657 | * @param ressource file handle 658 | * @param integer piece length 659 | * @param boolean is last piece 660 | * @return string pieces 661 | */ 662 | private function pieces ( $handle, $piece_length, $last = true ) { 663 | static $piece, $length; 664 | if ( empty( $length ) ) 665 | $length = $piece_length; 666 | $pieces = null; 667 | while ( ! feof( $handle ) ) { 668 | if ( ( $length = strlen( $piece .= fread( $handle, $length ) ) ) == $piece_length ) 669 | $pieces .= self::pack( $piece ); 670 | elseif ( ( $length = $piece_length - $length ) < 0 ) 671 | return self::set_error( new Exception( 'Invalid piece length!' ) ); 672 | } 673 | fclose( $handle ); 674 | return $pieces . ( $last && $piece ? self::pack( $piece ) : null); 675 | } 676 | 677 | /** Build torrent info from single file 678 | * @param string file path 679 | * @param integer piece length 680 | * @return array torrent info 681 | */ 682 | private function file ( $file, $piece_length ) { 683 | if ( ! $handle = self::fopen( $file, $size = self::filesize( $file ) ) ) 684 | return self::set_error( new Exception( 'Failed to open file: "' . $file . '"' ) ); 685 | if ( self::is_url( $file ) ) 686 | $this->url_list( $file ); 687 | $path = self::path_explode( $file ); 688 | return array( 689 | 'length' => $size, 690 | 'name' => end( $path ), 691 | 'piece length' => $piece_length, 692 | 'pieces' => $this->pieces( $handle, $piece_length ) 693 | ); 694 | } 695 | 696 | /** Build torrent info from files 697 | * @param array file list 698 | * @param integer piece length 699 | * @return array torrent info 700 | */ 701 | private function files ( $files, $piece_length ) { 702 | sort( $files ); 703 | usort( $files, create_function( '$a,$b', 'return strrpos($a,DIRECTORY_SEPARATOR)-strrpos($b,DIRECTORY_SEPARATOR);' ) ); 704 | $first = current( $files ); 705 | if ( ! self::is_url( $first ) ) 706 | $files = array_map( 'realpath', $files ); 707 | else 708 | $this->url_list( dirname( $first ) . DIRECTORY_SEPARATOR ); 709 | $files_path = array_map('self::path_explode', $files ); 710 | $root = call_user_func_array('array_intersect_assoc' , $files_path); 711 | $pieces = null; $info_files = array(); $count = count( $files ) - 1; 712 | foreach ( $files as $i => $file ) { 713 | if ( ! $handle = self::fopen( $file, $filesize = self::filesize( $file ) ) ) { 714 | self::set_error( new Exception( 'Failed to open file: "' . $file . '" discarded' ) ); 715 | continue; 716 | } 717 | $pieces .= $this->pieces( $handle, $piece_length, $count == $i ); 718 | $info_files[] = array( 719 | 'length' => $filesize, 720 | 'path' => array_diff_assoc( $files_path[$i], $root ) 721 | ); 722 | } 723 | return array( 724 | 'files' => $info_files, 725 | 'name' => end( $root ), 726 | 'piece length' => $piece_length, 727 | 'pieces' => $pieces 728 | ); 729 | 730 | } 731 | 732 | /** Build torrent info from folder content 733 | * @param string folder path 734 | * @param integer piece length 735 | * @return array torrent info 736 | */ 737 | private function folder ( $dir, $piece_length ) { 738 | return $this->files( self::scandir( $dir ), $piece_length ); 739 | } 740 | 741 | /** Helper to return the first char of encoded data 742 | * @param string encoded data 743 | * @return string|boolean first char of encoded data or false if empty data 744 | */ 745 | static private function char ( $data ) { 746 | return empty( $data ) ? 747 | false : 748 | substr( $data, 0, 1 ); 749 | } 750 | 751 | /**** Public Helpers ****/ 752 | 753 | /** Helper to format size in bytes to human readable 754 | * @param integer size in bytes 755 | * @param integer precision after coma 756 | * @return string formated size in appropriate unit 757 | */ 758 | static public function format ( $size, $precision = 2 ) { 759 | $units = array ('octets', 'Ko', 'Mo', 'Go', 'To'); 760 | while( ( $next = next( $units ) ) && $size > 1024 ) 761 | $size /= 1024; 762 | return round( $size, $precision ) . ' ' . ( $next ? prev( $units ) : end( $units ) ); 763 | } 764 | 765 | /** Helper to return filesize (even bigger than 2Gb -linux only- and distant files size) 766 | * @param string file path 767 | * @return double|boolean filesize or false if error 768 | */ 769 | static public function filesize ( $file ) { 770 | if ( is_file( $file ) ) 771 | return (double) sprintf( '%u', @filesize( $file ) ); 772 | else if ( $content_length = preg_grep( $pattern = '#^Content-Length:\s+(\d+)$#i', (array) @get_headers( $file ) ) ) 773 | return (int) preg_replace( $pattern, '$1', reset( $content_length ) ); 774 | } 775 | 776 | /** Helper to open file to read (even bigger than 2Gb, linux only) 777 | * @param string file path 778 | * @param integer|double file size (optional) 779 | * @return resource|boolean file handle or false if error 780 | */ 781 | static public function fopen ( $file, $size = null ) { 782 | if ( ( is_null( $size ) ? self::filesize( $file ) : $size ) <= 2 * pow( 1024, 3 ) ) 783 | return fopen( $file, 'r' ); 784 | elseif ( PHP_OS != 'Linux' ) 785 | return self::set_error( new Exception( 'File size is greater than 2GB. This is only supported under Linux' ) ); 786 | elseif ( ! is_readable( $file ) ) 787 | return false; 788 | else 789 | return popen( 'cat ' . escapeshellarg( realpath( $file ) ), 'r' ); 790 | } 791 | 792 | /** Helper to scan directories files and sub directories recursively 793 | * @param string directory path 794 | * @return array directory content list 795 | */ 796 | static public function scandir ( $dir ) { 797 | $paths = array(); 798 | foreach ( scandir( $dir ) as $item ) 799 | if ( $item != '.' && $item != '..' ) 800 | if ( is_dir( $path = realpath( $dir . DIRECTORY_SEPARATOR . $item ) ) ) 801 | $paths = array_merge( self::scandir( $path ), $paths ); 802 | else 803 | $paths[] = $path; 804 | return $paths; 805 | } 806 | 807 | /** Helper to check if string is an url (http) 808 | * @param string url to check 809 | * @return boolean is string an url 810 | */ 811 | static public function is_url ( $url ) { 812 | return preg_match( '#^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$#i', $url ); 813 | } 814 | 815 | /** Helper to check if url exists 816 | * @param string url to check 817 | * @return boolean does the url exist or not 818 | */ 819 | static public function url_exists ( $url ) { 820 | return self::is_url( $url ) ? 821 | (bool) self::filesize ( $url ) : 822 | false; 823 | } 824 | /** Helper to check if a file is a torrent 825 | * @param string file location 826 | * @param float http timeout (optional, default to self::timeout 30s) 827 | * @return boolean is the file a torrent or not 828 | */ 829 | static public function is_torrent ( $file, $timeout = self::timeout ) { 830 | return ( $start = self::file_get_contents( $file, $timeout, 0, 11 ) ) 831 | && $start === 'd8:announce' 832 | || $start === 'd10:created' 833 | || $start === 'd13:creatio' 834 | || $start === 'd13:announc' 835 | || $start === 'd12:_info_l' 836 | || substr($start, 0, 10) === 'd7:comment' // @see https://github.com/adriengibrat/torrent-rw/issues/32 837 | || substr($start, 0, 7) === 'd4:info' 838 | || substr($start, 0, 3) === 'd9:'; // @see https://github.com/adriengibrat/torrent-rw/pull/17 839 | } 840 | 841 | /** Helper to get (distant) file content 842 | * @param string file location 843 | * @param float http timeout (optional, default to self::timeout 30s) 844 | * @param integer starting offset (optional, default to null) 845 | * @param integer content length (optional, default to null) 846 | * @return string|boolean file content or false if error 847 | */ 848 | static public function file_get_contents ( $file, $timeout = self::timeout, $offset = null, $length = null ) { 849 | if ( is_file( $file ) || ini_get( 'allow_url_fopen' ) ) { 850 | $context = ! is_file( $file ) && $timeout ? 851 | stream_context_create( array( 'http' => array( 'timeout' => $timeout ) ) ) : 852 | null; 853 | return ! is_null( $offset ) ? $length ? 854 | @file_get_contents( $file, false, $context, $offset, $length ) : 855 | @file_get_contents( $file, false, $context, $offset ) : 856 | @file_get_contents( $file, false, $context ); 857 | } elseif ( ! function_exists( 'curl_init' ) ) 858 | return self::set_error( new Exception( 'Install CURL or enable "allow_url_fopen"' ) ); 859 | $handle = curl_init( $file ); 860 | if ( $timeout ) 861 | curl_setopt( $handle, CURLOPT_TIMEOUT, $timeout ); 862 | if ( $offset || $length ) 863 | curl_setopt( $handle, CURLOPT_RANGE, $offset . '-' . ( $length ? $offset + $length -1 : null ) ); 864 | curl_setopt( $handle, CURLOPT_RETURNTRANSFER, 1 ); 865 | $content = curl_exec( $handle ); 866 | $size = curl_getinfo( $handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD ); 867 | curl_close( $handle ); 868 | return ( $offset && $size == -1 ) || ( $length && $length != $size ) ? $length ? 869 | substr( $content, $offset, $length) : 870 | substr( $content, $offset) : 871 | $content; 872 | } 873 | 874 | /** Flatten announces list 875 | * @param array announces list 876 | * @return array flattened announces list 877 | */ 878 | static public function untier( $announces ) { 879 | $list = array(); 880 | foreach ( (array) $announces as $tier ) { 881 | is_array( $tier ) ? 882 | $list = array_merge( $list, self::untier( $tier ) ) : 883 | array_push( $list, $tier ); 884 | } 885 | return $list; 886 | } 887 | 888 | } 889 | -------------------------------------------------------------------------------- /examples/big.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | WebTorrent CDN Examples (PHP) BIG FILE 17 | 18 | 36 | 37 | 38 | 39 | 40 |

WebTorrent CDN Examples (PHP) BIG FILE

41 |

See the browser console :)

42 | file('ubuntu-17.04-desktop-amd64.iso'); 47 | 48 | /* Ubuntu http://ba.mirror.garr.it/mirrors/ubuntu-releases/17.04/ubuntu-17.04-desktop-amd64.iso.torrent */ 49 | 50 | ?> 51 | 52 | render(array('file1'=>$file1),'BIG FILE ubuntu-17.04-desktop-amd64 (not versioned)'); ?> 53 | 54 | 55 | 56 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /examples/codice-libero.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreapaiola/P2P-CDN/04c45051e77247d6769a6e80095a3c56a46fb046/examples/codice-libero.pdf -------------------------------------------------------------------------------- /examples/index.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | WebTorrent CDN Examples (PHP) 17 | 18 | 36 | 37 | 38 | 39 | 40 |

WebTorrent CDN Examples (PHP)

41 |

See the browser console :)

42 | file('IMG_20170218_122147549.jpg'); 47 | ?> 48 | 49 | render(array('file1'=>$file1),'Photo'); ?> 50 | 51 | 52 |

Output multiple times the same file

53 | 54 | 55 |

render(array('image'=>$file1),'Photo'); ?>

56 | 57 | 58 |

A more complex output with multiple files

59 | 60 | file('IMG_20170311_092817396.jpg'); ?> 61 | 62 | 64 | 66 | 68 | Photo 69 | 70 | Photo 74 | '; 75 | ?> 76 | 77 | render(array('img1'=>$file1,'img2'=>$file2),$html); ?> 78 | 79 | 80 |

A video with preview

81 | 82 | file('test.mp4'); ?> 83 | 84 | Sorry, your browser doesnt support embedded videos, 85 | but dont worry, you can download it 86 | and watch it with your favorite video player! 87 | '; ?> 88 | 89 | render(array('posterImg'=>$file1,'video'=>$file3),$html); ?> 90 | 91 | 92 | file('codice-libero.pdf'); ?> 93 | 94 |

Formatting a link to a PDF...

95 | 96 | = 1073741824) { 100 | $bytes = number_format($bytes / 1073741824, 2) . ' GB'; 101 | } elseif ($bytes >= 1048576) { 102 | $bytes = number_format($bytes / 1048576, 2) . ' MB'; 103 | } elseif ($bytes >= 1024) { 104 | $bytes = number_format($bytes / 1024, 2) . ' KB'; 105 | } elseif ($bytes > 1) { 106 | $bytes = $bytes . ' bytes'; 107 | } elseif ($bytes == 1) { 108 | $bytes = $bytes . ' byte'; 109 | } else { 110 | $bytes = '0 bytes'; 111 | } 112 | return $bytes; 113 | } 114 | ?> 115 | 116 | render(array('PDF'=>$pdf),' 117 | Codice Libero ('.formatFileSize($pdf['size']).') - last modified on '.date('l jS \of F Y h:i:s A',$pdf['lastmod']).''); ?> 118 | 119 | 120 |

Audio with autoplay

121 | file('AudioTest.ogg'); ?> 122 | 123 | render(array('audio'=>$audio),''); ?> 127 | 128 | 129 | 130 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /examples/t.php: -------------------------------------------------------------------------------- 1 | httpseeds($url); 52 | $torrent->url_list(array($url)); 53 | 54 | $torrent->send(); 55 | 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /examples/test.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreapaiola/P2P-CDN/04c45051e77247d6769a6e80095a3c56a46fb046/examples/test.mp4 -------------------------------------------------------------------------------- /namespaced/P2pcdn/P2pCdn.php: -------------------------------------------------------------------------------- 1 |