Using phar Stream to Write PharData Files - php

When I try this:
$phar = new PharData('./phar.tar', 0, null, Phar::TAR);
$phar->addEmptyDir('test');
file_put_contents('phar://./phar.tar/test/foo.txt', 'bar');
I get the following error:
Warning: file_put_contents(phar://./phar.tar/test/foo.txt): failed to
open stream: Cannot create phar './phar.tar', file extension (or
combination) not recognised or the directory does not exist
I'm able to use file_get_contents but shouldn't file_put_contents work too?
To make things even weirder, I tried the idea #hakre suggested in his comment and it works!
$phar = new PharData('./phar.tar', 0, null, Phar::TAR);
$phar->addEmptyDir('test');
if (file_exists('./phar.phar') !== true)
{
symlink('./phar.tar', './phar.phar');
}
file_put_contents('phar://./phar.phar/test/foo.txt', 'bar');
var_dump(file_get_contents('phar://./phar.tar/test/foo.txt')); // string(3) "bar"

Introduction
I think this basically has to do with the fact that you are working with Phar::TAR files because
file_put_contents("phar://./test.tar/foo.txt", "hello world");
^
|+---- Note this tar
Returns
Warning: file_put_contents(phar://./test.tar/foo.txt): failed to open stream: Cannot create phar './test.tar', file extension (or combination) not recognized or the directory does not exist
While
file_put_contents("phar://./test.phar/foo.txt", "hello world"); // Works file
^
|+---- Note This phar
Returns
//No Errors
Know Issue
If you look at a detail example in the PHP doc , this has been a known issue for the past 2 years and the example given are as follows
Example
$p = new PharData(dirname(__FILE__) . '/phartest.zip', 0, 'phartest', Phar::ZIP);
$p->addFromString('testfile.txt', 'this is just some test text');
// This works
echo file_get_contents('phar://phartest.zip/testfile.txt');
// This Fails
file_put_contents('phar://phartest.zip/testfile.txt', 'Thist is text for testfile.txt');
$context = stream_context_create(array(
'phar' => array(
'compress' => Phar::ZIP
)
));
// This Fails
file_put_contents('phar://phartest.zip/testfile.txt', 'Thist is text for testfile.txt', 0, $context);
// This works but only with 'r' readonly mode.
$f = fopen('phar://C:\\Inetpub\\wwwroot\\PACT\\test\\phartest.zip\\testfile.txt', 'r');
Working Alternative
Don't use .tar or .zip has your file extension but set compression as demonstrated in the PHP DOC
What do i mean ?
$context = stream_context_create(array(
'phar' => array(
'compress' => Phar::GZ //set compression
)
));
file_put_contents('phar://sample.phar/somefile.txt', "Sample Data", 0, $context);
^
|= Use phar not tar
Advice
I really think you should stick with PharData is realizable and proven to work. The only way to make
I must use file_put_contents
Then write your own wrapper and register it with stream_wrapper_register here is a simple Prof of Concept
stream_wrapper_register("alixphar", "AlixPhar");
file_put_contents('alixphar://phar.tar/test/foo.txt', 'hey');
^
|+-- Write with alixphar
echo file_get_contents('alixphar://phar.tar/test/foo.txt'),PHP_EOL;
echo file_get_contents('phar://phar.tar/test/foo.txt'),PHP_EOL;
Output
hey <----- alixphar
hey <----- phar
Note: This class should not be used in production ... It just an Example and would fail some test cases
class AlixPhar {
private $pos;
private $stream;
private $types;
private $dir;
private $pharContainer, $pharType, $pharFile, $pharDir = null;
private $fp;
function __construct() {
$this->types = array(
"tar" => Phar::TAR,
"zip" => Phar::ZIP,
"phar" => Phar::PHAR
);
// your phar dir to help avoid using path before the phar file
$this->dir = __DIR__;
}
private function parsePath($path) {
$url = parse_url($path);
$this->pharContainer = $url['host'];
$this->pharType = $this->types[pathinfo($this->pharContainer, PATHINFO_EXTENSION)];
if (strpos($url['path'], ".") !== false) {
list($this->pharDir, $this->pharFile, , ) = array_values(pathinfo($url['path']));
} else {
$this->pharDir = $url['path'];
}
}
public function stream_open($path, $mode, $options, &$opened_path) {
$this->parsePath($path);
$this->stream = new PharData($this->dir . "/" . $this->pharContainer, 0, null, $this->pharType);
if (! empty($this->pharDir))
$this->stream->addEmptyDir($this->pharDir);
if ($mode == "rb") {
$this->fp = fopen("phar://" . $this->pharContainer . "/" . $this->pharDir . "/" . $this->pharFile, $mode);
}
return true;
}
public function stream_write($data) {
$this->pos += strlen($data);
$this->stream->addFromString($this->pharDir . "/" . $this->pharFile, $data);
return $this->pos;
}
public function stream_read($count) {
return fread($this->fp, $count);
}
public function stream_tell() {
return ftell($this->fp);
}
public function stream_eof() {
return feof($this->fp);
}
public function stream_stat() {
return fstat($this->fp);
}
}

I have some answers for you - hopefully this will help you out.
First of all, with file_put_contents(), I've noticed some weirdness. First, what I did is follow the example of the link you posted. I was able to add the $context variable, but still had the error. What worked well for me was changing the file name to phar.phar. Perhaps the phar:// handler can't understand a .tar extension?
Anyway, this appears to be working code - but I don't recommend it
$context = stream_context_create(array('phar' =>
array('compress' => Phar::GZ)),
array('metadata' => array('user' => 'cellog')));
file_put_contents('phar://./phar.phar/test/foo.txt', 'bar', 0, $context);
Instead, use this.
Use me - I'm recommended
$phar = new PharData('./phar.tar', 0, null, Phar::TAR);
$phar->addEmptyDir('test');
$phar->addFile('./bar', 'foo.txt');
If you really need to use file_put_contents() instead, perhaps there's a different problem that exists that maybe we can help with. Thanks and good luck!

I've got a couple other alternative approaches which I think are a bit tidier. They avoid custom wrappers, symlinks or creating extra files in order to use the addFile function.
Option A
Use the addFromString function which works similarly to file_put_contents. The code below creates a TAR archive, adds a file to it and compresses it too.
$phar = new PharData("./archive.tar", 0, null, Phar::TAR);
$phar->addFromString('out/fileA.txt','The quick brown fox jumped over the lazy dog');
$phar->compress(Phar::GZ); # Creates archive.tar.gz
Option B
If you really need to use file_put_contents for some reason. You can create a PHAR archive using it, then reopen it and convert it to another archive type.
file_put_contents('phar://archive2.phar/out/fileB.txt', "Colourless green ideas sleep furiously");
$tarphar = new Phar('archive2.phar');
$tar = $tarphar->convertToData(Phar::TAR); # Creates archive2.tar
$zip = $tarphar->convertToData(Phar::ZIP); # Creates archive2.zip
$tgz = $tarphar->convertToData(Phar::TAR, Phar::GZ, '.tar.gz'); # Creates archive2.tar.gz

Related

How to not have tmpfile() be deleted when out of the scope of the method that it was created by?

I'm on php#8.1.3. When I have one method both creating and reading from a tmpfile, everything works as expected:
class TmpFileReadRightAway
{
public function storeToTempFileAndReadRightAway(string $content): string
{
$fh = tmpfile();
$path = stream_get_meta_data($fh)['uri'];
fwrite($fh, $content);
return file_get_contents($path);
}
}
echo (new TmpFileReadRightAway())->storeToTempFileAndReadRightAway('this works as expected');
Yet when I split the method into multiple methods, the tempfile() is deleted after the method in which it was created returns.
This is not at all what I expected as I wanted to keep the file around. I would expect the tmpfile to be deleted at termination of the php code at the very end, not after it exits the method.
class TmpFileStoreButReadLater
{
public function storeButReadLater(string $content): string
{
$path = $this->getPath($content);
return file_get_contents($path); // file at path doesn't exist anymore here, why?
}
private function getPath($content): string
{
$fh = tmpfile();
$path = stream_get_meta_data($fh)['uri'];
fwrite($fh, $content);
return $path;
}
}
This would throw
PHP Warning: file_get_contents(/tmp/phpQsUdA5): Failed to open stream: No such file or directory
Why is the file being deleted in this case and how do I ensure it exists during the runtime of my code?
Use class property.
The tmpfile() document said.
The file is automatically removed when closed (for example, by calling
fclose(), or when there are no remaining references to the file handle
returned by tmpfile()), or when the script ends.
So, I assume that when method exits, the fclose() is called automatically.
The error about failed to open stream is not just occur in PHP 8.1 but all version since PHP 7.0 to 8.1. (I don't have PHP 5.x to test with.)
To prevent that, set the $fh to class property instead.
class TmpFileStoreButReadLater
{
protected $fh;
public function storeButReadLater(string $content): string
{
$path = $this->getPath($content);
return file_get_contents($path); // file at path doesn't exist anymore here, why?
}
private function getPath($content): string
{
$this->fh = tmpfile();
$path = stream_get_meta_data($this->fh)['uri'];
fwrite($this->fh, $content);
return $path;
}
}
echo (new TmpFileStoreButReadLater())->storeButReadLater('this works as expected');
Tested on PHP 7.0 - 8.1.3 but no errors now.

Adding files to a Tar Archive in PHP with different filenames

I'm currently using the Archive-Tar Pear extension for PHP to add a collection of files into a Tar Archive.
These files are stored on a filer with an extra extension
e.g.
filename.tgz.104850209.t or filename2.doc.2154395.t
I'd like to remove this extra extension while adding the files so that my Tar Archive would have the files: filename.tgz and filename2.doc
Is there a way of doing that without having to copy/rename the source files first before adding to the Archive?
Thanks,
Mark.
Archive_Tar in its latest version does not yet support such a functionality out of the box. Part of the functionality is in _addFile() and the other part in _addString().
Most easy is probably to extend from Archive_Tar and proxy all calls to _writeHeaderBlock() which is public, applying a map on the filename parameter so to rename it when written into headers.
class Patched_Archive_Tar extends Archive_Tar
{
var $renameMap = array();
function _writeHeaderBlock($p_filename, $p_size, $p_mtime=0, $p_perms=0,
$p_type='', $p_uid=0, $p_gid=0)
{
return parent::_writeHeaderBlock($this->_translateFilename($p_filename),
$p_size, $p_mtime=0, $p_perms=0,
$p_type='', $p_uid=0, $p_gid=0);
}
function _translateFilename($orignal)
{
$map = $this->renameMap;
if (isset($map[$orignal])) {
return $map[$orignal];
}
return $original;
}
}
Usage:
$obj = new Patched_Archive_Tar('dummy.tar'); // name of archive
$files = array('mystuff/ad.gif',
'mystuff/alcon.doc.t',
'mystuff/alcon.xls.t'); // files to store in archive
$obj->renameMap = array(
'mystuff/alcon.doc.t' => 'mystuff/alcon.doc',
'mystuff/alcon.xls.t' => 'mystuff/alcon.xls',
) // files to rename
if ($obj->create($files)) {
echo 'Created successfully!';
} else {
echo 'Error in file creation';
}
This is quick and dirty but hopefully worky. For something better see the function I noticed at the beginning _addFile() and _addString(), you basically want another one that is able to add a file (as with _addFile()) by specifiying the filename (as with _addString()).
Tried to edit #hakre's answer, but peer reviewers weren't having that.
To answer #user2248522's comment, I rewrote the class to use _writeHeader. Additionally, I added a block for any Windows users out there and fixed a couple spelling errors.
class Patched_Archive_Tar extends Archive_Tar
{
var $renameMap = array();
function _writeHeader($p_filename, $p_stored_filename)
{
return parent::_writeHeader($p_filename,
$this->_translateFilename($p_stored_filename));
}
function _translateFilename($orignal)
{
$map = $this->renameMap;
if (isset($map[$original])) {
return $map[$original];
}
//Need alter our map array to match the altered original on WIN systems
if (defined('OS_WINDOWS') && OS_WINDOWS) {
//Check for a proper array
if (!is_array($map)) return $original;
//Check each replacement rule
foreach($map as $needle => $replacement) {
if ($this->_translateWinPath($needle, true) == $original) {
return $replacement;
} //if()
} //foreach()
} //if()
return $original;
}
}
Usage:
$obj = new Patched_Archive_Tar('dummy.tar'); // name of archive
$files = array('mystuff/ad.gif',
'mystuff/alcon.doc.t',
'mystuff/alcon.xls.t'); // files to store in archive
$obj->renameMap = array(
'mystuff/alcon.doc.t' => 'mystuff/alcon.doc',
'mystuff/alcon.xls.t' => 'mystuff/alcon.xls',
) // files to rename
if ($obj->create($files)) {
echo 'Created successfully!';
} else {
echo 'Error in file creation';
}

Creating a folder when I run file_put_contents()

I have uploaded a lot of images from the website, and need to organize files in a better way.
Therefore, I decide to create a folder by months.
$month = date('Yd')
file_put_contents("upload/promotions/".$month."/".$image, $contents_data);
after I tried this one, I get error result.
Message: file_put_contents(upload/promotions/201211/ang232.png): failed to open stream: No such file or directory
If I tried to put only file in exist folder, it worked. However, it failed to create a new folder.
Is there a way to solve this problem?
file_put_contents() does not create the directory structure. Only the file.
You will need to add logic to your script to test if the month directory exists. If not, use mkdir() first.
if (!is_dir('upload/promotions/' . $month)) {
// dir doesn't exist, make it
mkdir('upload/promotions/' . $month);
}
file_put_contents('upload/promotions/' . $month . '/' . $image, $contents_data);
Update: mkdir() accepts a third parameter of $recursive which will create any missing directory structure. Might be useful if you need to create multiple directories.
Example with recursive and directory permissions set to 777:
mkdir('upload/promotions/' . $month, 0777, true);
modification of above answer to make it a bit more generic, (automatically detects and creates folder from arbitrary filename on system slashes)
ps previous answer is awesome
/**
* create file with content, and create folder structure if doesn't exist
* #param String $filepath
* #param String $message
*/
function forceFilePutContents ($filepath, $message){
try {
$isInFolder = preg_match("/^(.*)\/([^\/]+)$/", $filepath, $filepathMatches);
if($isInFolder) {
$folderName = $filepathMatches[1];
$fileName = $filepathMatches[2];
if (!is_dir($folderName)) {
mkdir($folderName, 0777, true);
}
}
file_put_contents($filepath, $message);
} catch (Exception $e) {
echo "ERR: error writing '$message' to '$filepath', ". $e->getMessage();
}
}
i have Been Working on the laravel Project With the Crud Generator and this Method is not Working
#aqm so i have created my own function
PHP Way
function forceFilePutContents (string $fullPathWithFileName, string $fileContents)
{
$exploded = explode(DIRECTORY_SEPARATOR,$fullPathWithFileName);
array_pop($exploded);
$directoryPathOnly = implode(DIRECTORY_SEPARATOR,$exploded);
if (!file_exists($directoryPathOnly))
{
mkdir($directoryPathOnly,0775,true);
}
file_put_contents($fullPathWithFileName, $fileContents);
}
LARAVEL WAY
Don't forget to add at top of the file
use Illuminate\Support\Facades\File;
function forceFilePutContents (string $fullPathWithFileName, string $fileContents)
{
$exploded = explode(DIRECTORY_SEPARATOR,$fullPathWithFileName);
array_pop($exploded);
$directoryPathOnly = implode(DIRECTORY_SEPARATOR,$exploded);
if (!File::exists($directoryPathOnly))
{
File::makeDirectory($directoryPathOnly,0775,true,false);
}
File::put($fullPathWithFileName,$fileContents);
}
I created an simpler answer from #Manojkiran.A and #Savageman. This function can be used as drop-in replacement for file_put_contents. It doesn't support context parameter but I think should be enough for most cases. I hope this helps some people. Happy coding! :)
function force_file_put_contents (string $pathWithFileName, mixed $data, int $flags = 0) {
$dirPathOnly = dirname($pathWithFileName);
if (!file_exists($dirPathOnly)) {
mkdir($dirPathOnly, 0775, true); // folder permission 0775
}
file_put_contents($pathWithFileName, $data, $flags);
}
Easy Laravel solution:
use Illuminate\Support\Facades\File;
// If the directory does not exist, it will be create
// Works recursively, with unlimited number of subdirectories
File::ensureDirectoryExists('my/super/directory');
// Write file content
File::put('my/super/directory/my-file.txt', 'this is file content');
I wrote a function you might like. It is called forceDir(). It basicaly checks whether the dir you want exists. If so, it does nothing. If not, it will create the directory. A reason to use this function, instead of just mkdir, is that this function can create nexted folders as well.. For example ('upload/promotions/januari/firstHalfOfTheMonth'). Just add the path to the desired dir_path.
function forceDir($dir){
if(!is_dir($dir)){
$dir_p = explode('/',$dir);
for($a = 1 ; $a <= count($dir_p) ; $a++){
#mkdir(implode('/',array_slice($dir_p,0,$a)));
}
}
}

Flag for just create an empty file if not exists?

Which flag should i use for create a file if not exists? Please not that i'll close the point right after fopen() because the "hard part" (decoding the encrypted content) is carried by load() function (decoding logic is not shown):
Class MyClass
{
protected $filename, $data;
public function __construct($filename)
{
$this->filename = $filename;
// Create if not exists
if(!file_exists($this->filename))
{
$fp = fopen($this->filename, '');
fclose($fp);
}
$this->load();
}
public function load()
{
$data = file_get_contents($this->filename);
$this->data = $data === false ? array() : $data;
}
}
wb is about all you'd need. Open a file for writing, truncate any file which already exists, set the file pointer to the start of this new file, and enable binary mode (which prevents PHP from translating line-ending characters on certain platforms like Windows).
'a+', here manual. Ensure that permissions for the dir is ok.

PHP, Check if URL and a file exists ?

I create a plugin for WordPress that requires two files to be exists in order to operate normaly.
The first file is defined as a file system path and the second file is defined as a URL.
Let's say the first file is that:
/home/my_site/public_html/some_folder/required_file.php
and the second file is that:
http://www.my_site.com/some_folder/required_url_file.php
Note that both files are not the same file into the file system. The required_file.php has other content than the required_url_file.php and they act absolutly diferent
Any idea on how to validate the existance of both files ?
You can check both:
$file = '/home/my_site/public_html/some_folder/required_file.php';
$url = 'http://www.my_site.com/some_folder/required_url_file.php';
$fileExists = is_file($file);
$urlExists = is_200($url);
$bothExists = $fileExists && $urlExists;
function is_200($url)
{
$options['http'] = array(
'method' => "HEAD",
'ignore_errors' => 1,
'max_redirects' => 0
);
$body = file_get_contents($url, NULL, stream_context_create($options));
sscanf($http_response_header[0], 'HTTP/%*d.%*d %d', $code);
return $code === 200;
}
Based on Maor H. code sample, here is a function I am using in my plugins:
/**
* Check if an item exists out there in the "ether".
*
* #param string $url - preferably a fully qualified URL
* #return boolean - true if it is out there somewhere
*/
function webItemExists($url) {
if (($url == '') || ($url == null)) { return false; }
$response = wp_remote_head( $url, array( 'timeout' => 5 ) );
$accepted_status_codes = array( 200, 301, 302 );
if ( ! is_wp_error( $response ) && in_array( wp_remote_retrieve_response_code( $response ), $accepted_status_codes ) ) {
return true;
}
return false;
}
I've made this a method in a helper class, however putting this in your theme's functions.php file should make it generally accessible everywhere. However you should always be writing in classes and instantiating them. It is much better for isolating your plugin and theme functionality.
With this in place you can simply use:
if (webItemExists('http://myurl.com/thing.png')) {
print 'it iexists';
}
Most often you will be using WordPress calls to access all items via a relative or fully qualified URL. If you have a relative reference to something such as /uploads/2012/12/myimage.png you can convert those to a fully qualified URL v. a WordPress relative URL by simply adding get_site_url(). $string when calling the webItemExists() function.
As for validating the URL, none of these answers are considering the correct, WordPress way to carry out this task.
For this task wp_remote_head() should be used.
Here's an article I've written about How To Check Whether an External URL Exists with WordPress’ HTTP API. Check it out and figure out how it works.
$file_exists = file_exists($path);
$url_accessable = http_get($url, array("timeout"=>10), $info); // should not be FALSE
$status_code = $info['response_code'] //should be 200
This seems to work for me:
function url_file_exists($url) {
$context = stream_context_create(array('http' =>array('method'=>'HEAD')));
$fd = #fopen($url, 'rb', false, $context);
if ($fd!==false) {
fclose($fd);
return true;
}
return false;
}
If you have PECL http_head function available, you could check if it returns status code 200 for the remote file.
To check if you can access the local file, could use file_exists, but this does not grant that you will be able to access that file. To check if you can read that file, use is_readable.
To check if a file exists, use the file_exists method.
As of PHP 5.0.0, this function can also be used with some URL
wrappers. Refer to Supported Protocols and Wrappers to determine which
wrappers support stat() family of functionality.
if(! (file_exists($url1) && file_exists($url2)) ) {
die("Files don't exist - throw error here.");
}
// Continue as usual - files exist at this point.
remote:
$file = 'http://www.my_site.com/some_folder/required_url_file.php'
if ( #fclose(#fopen($file,"r")) ) echo "File exists!";
local:
$file = '/home/my_site/public_html/some_folder/required_file.php';
if ( is_file($file) ) echo "File exists!";
Use function file_exists()
file_exists('http://www.my_site.com/some_folder/required_url_file.php');
will get you results as True or false.
Checking if a file exists:
if (file_exists('path/to/file.txt')) {
echo "File exists!";
} else {
echo "File doesn't exist.";
}
Checking if a URL is valid:
$data = #file_get_contents("http://url.com/");
if (!$data) {
echo "URL not valid.";
} else {
echo "URL is valid.";
}
Notes:
Ideally you shouldn't try and predict the filesystem. Whilst methods such as file_exists are very helpful, they shouldn't be relied upon and instead you should attempt to write to files, read from them, etc, and then catch and handle any exceptions or errors that occur.

Categories