Jump to content
dragan

Download all files as ZIP from inputfield file?

Recommended Posts

In page-edit mode, it would be nice to have an optional download-button for inputfield type file.

i.e. when a user has uploaded two dozen files (perhaps through the frontend), and someone else needs to download these from the backend all at once, without having to open an FTP client. There would simply be a download link / button, and a ZIP would be generated on the fly, containing all files.

If someone has an idea how to do that (a hook?), I'd be all ears :D

  • Like 1

Share this post


Link to post
Share on other sites

Here are some starters:

  • Hook into InputfieldFile::render method to add (modify $event->return and concatenate html string) an anchor link with its href set to a custom template/PHP file where you build your own zip file.
    (Preferentially, add a form with a download button and a hidden field for current page id etc and set its action attribute to the template file)
  • Inside the template file, you'll get the page id from $input->post then build your zip file
  • foreach($page->files_field as $name => $file) {
        $filePath = $file->path
        // ...
    }
    Use $files->zip() method to zip multiple files
    https://processwire.com/api/ref/files/zip/

    or (from /wire/core/WireFileTools.php)
     
    // Create zip of all files in directory $dir to file $zip
    $dir = $config->paths->cache . "my-files/"; 
    $zip = $config->paths->cache . "my-file.zip";
    $result = $files->zip($zip, $dir); 
     
    echo "<h3>These files were added to the ZIP:</h3>";
    foreach($result['files'] as $file) {
      echo "<li>" $sanitizer->entities($file) . "</li>";
    }
    
    if(count($result['errors'])) {
      echo "<h3>There were errors:</h3>";
      foreach($result['errors'] as $error) {
        echo "<li>" . $sanitizer->entities($error) . "</li>";
      }
    }
    and build your zip file
  • Once the zip file is built force browser to download
    https://stackoverflow.com/a/1628281

If you have any questions, ask away

  • Like 5

Share this post


Link to post
Share on other sites

Excellent! Thanks a lot. Will try it this weekend.

Share this post


Link to post
Share on other sites

Have done this for image fields, it adds a button to the end of the inputfield, code below. Checks for ZipArchive class before installing. Now that I read the code it should work for files but haven't tested! It's got configuration options too, to enable per field basis, either way should it should be super quick to edit if something is not working.

<?php

class ExportFieldImages extends WireData implements Module, ConfigurableModule {
    
    static public function getDefaultConfig() {
        return array(
            "includeFields" => ''
        );
    }

    public static function install(){
        $session = wire("session");
        if(!class_exists('\ZipArchive')){
            throw new WireException("ZipArchive is required to create the zip file");
        }
    }
    
    public static function getModuleInfo() {
        return array(
            'title' => 'ExportFieldImages',
            'version' => 0.1, 
            'summary' => "Adds a button bellow images field to download a zip containing files.",
            'author' => 'Eduardo San Miguel Garcia',
            'singular' => true,
            'href' => '',
            'autoload' => true
        );
    }

    public function init() {
        $this->addHookAfter('InputfieldImage::render', $this, 'afterInputfieldImageRender');
        $this->addHookAfter("ProcessPageEdit::execute", $this, "pageEditExecuteBefore");
    }

    public function pageEditExecuteBefore(HookEvent $event){
        $session = wire("session");
        $input = wire("input");
        $page = wire('pages')->get($input->get->id);
        $name = $page->name;
        $zip = $config->paths->cache . "${$name}.zip";
        
        if($input->get->downloadImages == "true"){
            wire("log")->save("custom", "excecuted page edit!");
            $array = wire('files')->zip($zip,
                                        $page->get($input->get->field)->explode("filename"),
                                        array("overwrite" => true)  );
            if(!empty($array["errors"])){
                foreach($array["errors"] as $error){
                    $this->error($error);
                }
                return;
            } else{
                header('Content-Type: application/zip');
                header("Content-Disposition: attachment; filename='{$name}.zip'");
                echo readfile($zip);
                return $this->halt();
            }
            
        }
    }
    
    public function afterInputfieldImageRender(HookEvent $event){
        $field = $event->object;
        $input = wire("input");
        $configData = wire('modules')->getModuleConfigData($this);
      
        $out = $event->return;
        
        if(in_array( $field->name, $configData['includeFields'])){

            $button = wire('modules')->get('InputfieldButton');
            $button->class = $button->class . " ExportFieldImages";
            $button->icon = 'download';
            $button->href = "./?downloadImages=true&field={$field->name}&id={$input->get->id}";
            $button->value = "Download Files";
            $btn .= "<span style='display:block; position:relative; float:right;' class='InputfieldPageTableButtons'>" . $button->render() . "</span>";
            $out .= $btn;
        }
        
        $event->return = $out;
        
    }
    
    public function getModuleConfigInputfields(array $data){
        $modules = wire("modules");
        $fields = wire('fields');
        
        $defaults = self::getDefaultConfig();
		$data = array_merge($defaults, $data);
        
        $form = new InputfieldWrapper();
        
        $field = $modules->get("InputfieldAsmSelect");
		$field->name = "includeFields";
		$field->label = __("Fields to enable download button");
		$field->description = __("Choose the image fields where download button should appear");
        foreach($fields as $f){
            if($f->flags & Field::flagSystem) continue;
            if($f->type == "FieldtypeImage" || $f->type == "FieldtypeFile"){
                $field->addOption($f->name);
            }
        }
		$field->value = $data['includeFields'];
		
		$form->add($field);
        return $form;
    }
    
}

 

  • Like 4

Share this post


Link to post
Share on other sites
On 8/11/2017 at 12:28 PM, abdus said:

$file->path

Should be $file->filename or $file->filename() :-)

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By Sébastien
      Hi Guys,
      I recently started using ProcessWire and already manage to accomplish several things on my own, but I keep asking myself a lot of questions, among which the following:
      After installing a multiple sites version (first method), everything works for now as desired, but I realize that all the files seem accessible by all the domains.
      Let's say that site1.com can access the asset files, template or other ressources of site2.com folders and vice versa by specifying only and for example:
      site1.com/path_to_a_site2_folder/one_file
      or
      site2.com/path_to_a_site1_folder/one_file
      Would you know if there is a simple way to prohibit this kind of behavior in order to clearly distinguish each site and thus ensure the confidentiality and separation of content?
      I prefer to avoid the entities to be considered as a single set by crawlers and prevent access the elements belonging to each domain from another domain.
      Thanks for your help. Sébastien.
    • By gebeer
      Hello all,
      wasn't sure where to put this, so it goes in General section.
      Ryan shows a hook that we can use to mirror files on demand from live server to development environment to be up to date with the files on the server without having to download complete site/assets/files folder.
      I just implemented this but had problems getting files to load from a site in development that is secured with user/password via htaccess.
      First I tried to use WireHttp setHeader method for basic authentication like this
      function mirrorFilesfromLiveServer(HookEvent $event) { $config = $event->wire('config'); $file = $event->return; if ($event->method == 'url') { // convert url to disk path $file = $config->paths->root . substr($file, strlen($config->urls->root)); } if (!file_exists($file)) { // download file from source if it doesn't exist here $src = 'http://mydomain.com/site/assets/files/'; $url = str_replace($config->paths->files, $src, $file); $http = new WireHttp(); // basic authentication $u = 'myuser'; $pw = 'mypassword'; $http->setHeader('Authorization: Basic', base64_encode("$u:$pw")); $http->download($url, $file); } } But, unfortunately this didn't work.
      So now I am using curl to do the download. My hook function now looks like this
      function mirrorFilesfromLiveServer(HookEvent $event) { $config = $event->wire('config'); $file = $event->return; if ($event->method == 'url') { // convert url to disk path $file = $config->paths->root . substr($file, strlen($config->urls->root)); } if (!file_exists($file)) { // download file from source if it doesn't exist here $src = 'http://mydomain.com/site/assets/files/'; $fp = fopen($file, 'w+'); // init file pointer $url = str_replace($config->paths->files, $src, $file); $u = 'myuser'; $pw = 'mypassword'; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_TIMEOUT, 50); // crazy high timeout just in case there are very large files curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_USERPWD, "$u:$pw"); // authentication curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); // authentication curl_setopt($ch, CURLOPT_FILE, $fp); // give curl the file pointer so that it can write to it curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $data = curl_exec($ch); curl_close($ch); } } Now I can load files and images from the htaccess protected development server 🙂
      If anyone knows how to get this to work with WireHttp, please let me know. Thank you.
    • By Robin S
      If you've ever needed to insert links to a large number of files within CKEditor you may have found that the standard PW link modal is a somewhat slow way to do it.
      This module provides a quicker way to insert links to files on the page being edited. You can insert a link to an individual file, or insert an unordered list of links to all files on the page with a single click.
      CKEditor Link Files
      Adds a menu to CKEditor to allow the quick insertion of links to files on the page being edited.

      Features
      Hover a menu item to see the "Description" of the corresponding file (if present). Click a menu item to insert a link to the corresponding file at the current cursor position. The filename is used as the link text. If you Alt-click a menu item the file description is used as the link text (with fallback to filename if no description entered). If text is currently selected in the editor then the selected text is used as the link text. Click "* Insert links to all files *" to insert an unordered list of links to all files on the page. Also works with the Alt-click option. Menu is built via AJAX so newly uploaded files are included in the menu without the page needing to be saved. However, descriptions are not available for newly uploaded files until the page is saved. There is an option in the module config to include files from Repeater fields in the edited page. Nested Repeater fields (files inside a Repeater inside another Repeater) are not supported. Installation
      Install the CKEditor Link Files module.
      For any CKEditor field where you want the "Insert link to file" dropdown menu to appear in the CKEditor toolbar, visit the field settings and add "LinkFilesMenu" to the "CKEditor Toolbar" settings field.
       
      http://modules.processwire.com/modules/cke-link-files/
      https://github.com/Toutouwai/CkeLinkFiles
    • By neophron
      Hi,
      I was searching in the forum about a method how to remove a file extension, like .jpg, but couldn't find anything.
      There are some php methods out in the wild, but I wanted first to get sure, that Processwire is offering a solution.
      The background is, that a clients website has a lot of images, packed in different galleries. All image files contain the name, dimensions and other stuff about the image (painting). I want to echo the image name in the image tag.
       
      thanks 
    • By Sergio
      Hi everyone,
      Yesterday I began working on a module to create a filesystem abstraction for files and images using the Flysytem library (http://flysystem.thephpleague.com), out of the necessity of having the images copied (and probably linked) on Amazon S3 and other places like Dropbox. There two reasons why I decided to tackle this:
      1 - When I work on the project in my machine, I need a way to automatically sync the images from the server and/or the external storage.
      When an image is added on the server and the database is imported on my local env, PW shows a blank thumbnail of it. The idea for the module if to check if the page image has a width == 0 and if it exists on the server, add it to the local filesystem.
      2 - In the past, I had to move a large website to a different server in a hurry and a lot of images were left behind (it was a mess). So I'm planning for a possible future worst-case scenario of the server exploding 🙂 
      The code I quickly wrote is below (please bear with me that it's pretty raw at the moment). One thing I had to figure it out is why PW fires the Pageimage::size hook wherever a page is loaded in the admin, even though the thumbnails are already created. I was planning to save the image variations on S3 as well. Can someone clarify?
      I know that @LostKobrakai was working on a similar project, and so I would like to ask him and everyone else if you think I (and who may help) should evolve this idea into a full-featured module where the user can select which server (adapter) to use (AWS, Digital Ocean spaces, Dropbox etc.)
      <?php namespace ProcessWire; use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; use League\Flysystem\Filesystem; use League\Flysystem\Adapter\Local; use Spatie\Dropbox\Client; use Spatie\FlysystemDropbox\DropboxAdapter; class ProcessFly extends WireData implements Module, ConfigurableModule { public static function getModuleInfo() { return array( 'title' => 'ProcessWire Flysystem Integration', 'version' => 001, 'summary' => 'Synchronize all the page assets uploaded through PW to a specified bucket in Amazon S3 and...', 'author' => 'Sérgio Jardim', 'singular' => true, 'autoload' => true, 'icon' => 'image' ); } public function init() { $this->client = S3Client::factory([ 'credentials' => [ 'key' => '', 'secret' => '', ], 'region' => 'sa-east-1', 'version' => 'latest', ]); $this->bucket_name = ""; $this->dir_name = "images"; $this->s3_adapter = new AwsS3Adapter($this->client, $this->bucket_name, $this->dir_name); $this->s3_filesystem = new Filesystem($this->s3_adapter); // DROPBOX $this->authorizationToken = ""; $this->dropbox_client = new Client($this->authorizationToken); $this->adapter_dropbox = new DropboxAdapter($this->dropbox_client); $this->dropbox_filesystem = new Filesystem($this->adapter_dropbox); $this->addHookAfter('Pages::saved', $this, 'checkImageS3'); // Download images that are not in local filesystem for whatever reason but are available in the remove one. $this->addHookAfter('InputfieldFile::fileAdded', $this, 'uploadImageS3'); // Fired when a file/image is added to a page in the admin // $this->addHookAfter('Pageimage::size', $this, 'uploadImageS3'); // Fired when a file is resized via API. Note: this hook is also called when the page is loaded in Admin. Need to know why. $this->addHookAfter('Pageimage::url', $this, 'redirectImageURL'); //Replace image URL for the S3 path } public function redirectImageURL($event){ if($event->page->template == 'admin') return; else $event->return = "https://s3-sa-east-1.amazonaws.com/[bucket name]/images/" . $event->object->page . "/" . $event->object->name; } // UPLOAD public function uploadImageS3($event){ if(count($event->return)) { //if it is a image resize event get image variation data $file = $event->return; } else { $file = $event->arguments(0); } $filename = $file->name; $filename_original = $file->url; $pathToFile = $file->page . "/" . $filename; $system_path = $file->filename(); try{ $file_on_s3 = $this->s3_filesystem->has($pathToFile); //check if file exists on AWS if(!$file_on_s3) { $contents = file_get_contents($system_path); $this->s3_filesystem->put($pathToFile, $contents, array( 'visibility' => 'public', )); //upload file with the same folder structure as in assets/files: page_id/file_name //Also add a copy to Dropbox (OPTIONAL) $this->dropbox_filesystem->put($pathToFile, $contents); } } catch (Exception $e){ throw new WireException("Error: Image not Added to S3: {$e->getMessage()}"); } } public function checkImageS3($event){ $page = $event->arguments(0); if(count($page->images)) { foreach($page->images as $file) { if($file->width == 0) { return $this->downloadImageS3($page, $file); }; } } } public function downloadImageS3($page, $file){ $pathToFile = $page->id . "/" . $file->name; $file_on_s3 = $this->s3_filesystem->has($pathToFile); //check if file exists on AWS if($file_on_s3) { $image = "https://s3-sa-east-1.amazonaws.com/[bucket name]/images/" . $pathToFile; // $page->images->remove($image); $page->images->add($image); $page->save(); } else { throw new WireException("Error: Image not found on S3: {$file->name}"); } } }  
×
×
  • Create New...