Git Product home page Git Product logo

laravel-paperclip's Introduction

Latest Version on Packagist Software License Build Status Coverage Status

Laravel Paperclip: File Attachment Solution

Allows you to attach files to Eloquent models.

This is a re-take on CodeSleeve's Stapler. It is mainly intended to be more reusable and easier to adapt to different Laravel versions. Despite the name, this should not be considered a match for Ruby's Paperclip gem.

Instead of tackling file storage itself, it uses Laravel's internal storage drivers and configuration.

This uses czim/file-handling under the hood, and any of its (and your custom written) variant manipulations may be used with this package.

Version Compatibility

Laravel Package PHP Version
5.4 and below 1.0, 2.1 7.4 and below
5.5 1.5, 2.5 7.4 and below
5.6, 5.7 2.6 7.4 and below
5.8, 6 2.7 7.4 and below
7, 8 3.2 7.4 and below
7, 8, 9 4.0 8.0 and up
9 and up 5.0 8.1 and up

Change log

View the changelog.

Installation

Via Composer:

$ composer require czim/laravel-paperclip

Auto-discover may be used to register the service provider automatically. Otherwise, you can manually register the service provider in config/app.php:

<?php
   'providers' => [
        ...
        Czim\Paperclip\Providers\PaperclipServiceProvider::class,
        ...
   ],

Publish the configuration file:

php artisan vendor:publish --provider="Czim\Paperclip\Providers\PaperclipServiceProvider"

Set up and Configuration

Model Preparation

Modify the database to add some columns for the model that will get an attachment. Use the attachment key name as a prefix.

An example migration:

<?php
    Schema::create('your_models_table', function (Blueprint $table) {
        $table->string('attachmentname_file_name')->nullable();
        $table->integer('attachmentname_file_size')->nullable();
        $table->string('attachmentname_content_type')->nullable();
        $table->timestamp('attachmentname_updated_at')->nullable();
    });

Replace attachmentname here with the name of the attachment. These attributes should be familiar if you've used Stapler before.

A <key>_variants text or varchar column is optional:

<?php
    $table->string('attachmentname_variants', 255)->nullable();

A text() column is recommended in cases where a seriously huge amount of variants are created.

If it is added and configured to be used (more on that in the config section), JSON information about variants will be stored in it.

Attachment Configuration

To add an attachment to a model:

  • Make it implement Czim\Paperclip\Contracts\AttachableInterface.
  • Make it use the Czim\Paperclip\Model\PaperclipTrait.
  • Configure attachments in the constructor (very similar to Stapler)
<?php
class Comment extends Model implements \Czim\Paperclip\Contracts\AttachableInterface
{
    use \Czim\Paperclip\Model\PaperclipTrait;

    public function __construct(array $attributes = [])
    {
        $this->hasAttachedFile('image', [
            'variants' => [
                'medium' => [
                    'auto-orient' => [],
                    'resize'      => ['dimensions' => '300x300'],
                ],
                'thumb' => '100x100',
            ],
            'attributes' => [
                'variants' => true,
            ],
        ]);

        parent::__construct($attributes);
    }
}

Note: If you perform the hasAttachedFile() call(s) after the parent::__construct() call, everything will work the same, except that you cannot assign an image directly when creating a model. ModelClass::create(['attachment' => ...]) will not work in that case.

Since version 2.5.7 it is also possible to use an easier to use fluent object syntax for defining variant steps:

<?php
    use \Czim\Paperclip\Config\Variant;
    use \Czim\Paperclip\Config\Steps\AutoOrientStep;
    use \Czim\Paperclip\Config\Steps\ResizeStep;

    // ...

    $this->hasAttachedFile('image', [
        'variants' => [
            Variant::make('medium')->steps([
                AutoOrientStep::make(),
                ResizeStep::make()->width(300)->height(150)->crop(),
            ]),
            Variant::make('medium')->steps(ResizeStep::make()->square(100)),
        ],
    ]);

Variant Configuration

For the most part, the configuration of variants is nearly identical to Stapler, so it should be easy to make the transition either way.

Since version 2.6, Stapler configuration support is disabled by default, but legacy support for this may be enabled by setting the paperclip.config.mode to 'stapler'.

Get more information on configuration here.

Custom Variants

The file handler comes with a few common variant strategies, including resizing images and taking screenshots from videos. It is easy, however, to add your own custom strategies to manipulate files in any way required.

Variant processing is handled by the file-handler package. Check out its source to get started writing custom variant strategies.

Storage configuration

You can configure a storage location for uploaded files by setting up a Laravel storage (in config/filesystems.php), and registering it in the config/paperclip.php config file.

Make sure that paperclip.storage.base-urls.<your storage disk> is set, so valid URLs to stored content are returned.

Hooks Before and After Processing

It is possible to 'hook' into the paperclip goings on when files are processed. This may be done by using the before and/or after configuration keys. Before hooks are called after the file is uploaded and stored locally, but before variants are processed; after hooks are called when all variants have been processed.

More information and examples are in the Config section.

Events

The following events are available:

  • AttachmentSavedEvent: dispatched when any attachment is saved with a file

Refreshing models

When changing variant configurations for models, you may reprocess variants from previously created attachments with the paperclip:refresh Artisan command.

Example:

php artisan paperclip:refresh "App\Models\BlogPost" --attachments header,background

Usage

Once a model is set up and configured for an attachment, you can simply set the attachment attribute on that model to create an attachment.

<?php
public function someControllerAction(Request $request) {

    $model = ModelWithAttachment::first();

    // You can set any UploadedFile instance from a request on
    // the attribute you configured a Paperclipped model for.
    $model->attachmentname = $request->file('uploaded');

    // Saving the model will then process and store the attachment.
    $model->save();

    // ...
}

Setting attachments without uploads

Usually, you will want to set an uploaded file as an attachment. If you want to store a file from within your application, without the context of a request or a file upload, you can use the following approach:

<?php
// You can use the built in SplFileInfo class:
$model->attachmentname = new \SplFileInfo('local/path/to.file');


// Or a file-handler class that allows you to override values:
$file = new \Czim\FileHandling\Storage\File\SplFileInfoStorableFile();
$file->setData(new \SplFileInfo('local/path/to.file'));
// Optional, will be derived from the file normally
$file->setMimeType('image/jpeg');
// Optional, the file's current name will be used normally
$file->setName('original-file-name.jpg');
$model->attachmentname = $file;


// Or even a class representing raw content
$raw = new \Czim\FileHandling\Storage\File\RawStorableFile();
$raw->setData('... string with raw content of file ...');
$raw->setMimeType('image/jpeg');
$raw->setName('original-file-name.jpg');
$model->attachmentname = $raw;

Clearing attachments

In order to prevent accidental deletion, setting the attachment to null will not destroy a previously stored attachment. Instead you have to explicitly destroy it.

<?php
// You can set a special string value, the deletion hash, like so:
$model->attachmentname = \Czim\Paperclip\Attachment\Attachment::NULL_ATTACHMENT;
// In version 2.5.5 and up, this value is configurable and available in the config:
$model->attachmentname = config('paperclip.delete-hash');

// You can also directly clear the attachment by flagging it for deletion:
$model->attachmentname->setToBeDeleted();


// After any of these approaches, saving the model will make the deletion take effect.
$model->save();

Differences with Stapler

  • Paperclip does not handle (s3) storage internally, as Stapler did. All storage is performed through Laravel's storage solution. You can still use S3 (or any other storage disk), but you will have to configure it in Laravel's storage configuration first. It is possible to use different storage disks for different attachments.

  • Paperclip might show slightly different behavior when storing a string value on the attachment attribute. It will attempt to interpret the string as a URI (or a dataURI), and otherwise treat the string as raw text file content.

If you wish to force storing the contents of a URL without letting Paperclip interpret it, you have some options. You can use the Czim\FileHandling\Storage\File\StorableFileFactory@makeFromUrl method and its return value. Or, you can download the contents yourself and store them in a Czim\FileHandling\Storage\File\RawStorableFile (e.g.: (new RawStorableFile)->setData(file_get_contents('your-URL-here'))). You can also download the file to local disk, and store it on the model through an \SplFileInfo instance (see examples on the main readme page).

  • The convert_options configuration settings are no longer available. Conversion options are now handled at the level of the variant strategies. You can set them per attachment configuration, or modify the variant strategy to use a custom global configuration.

  • The refresh command (php artisan paperclip:refresh) is very similar to stapler's refresh command,

  • but it can optionally take a --start # and/or --stop # option, with ID numbers. This makes it possible to refresh only a subset of models. Under the hood, the refresh command is also much less likely to run out of memory (it uses a generator to process models in chunks).

  • The Paperclip trait uses its own Eloquent boot method, not the global Model's boot(). This makes Paperclip less likely to conflict with other traits and model implementations.

Amazon S3 cache-control

If you use Amazon S3 as storage disk for your attachments, note that you can set Cache-Control headers in the options for the filesystems.disks.s3 configuration key. For example, to set max-age headers on all uploaded files to S3, edit config/filesystems.php like so:

's3' => [
    'driver' => env('S3_DRIVER', 's3'),
    'key'    => env('S3_KEY', 'your-key'),
    'secret' => env('S3_SECRET', 'your-secret'),
    'region' => env('S3_REGION', 'your-region'),
    'bucket' => env('S3_BUCKET', 'your-bucket'),
    'visibility' => 'public',
    'options' => [
        'CacheControl' => 'max-age=315360000, no-transform, public',
    ],
],

Upgrade Guide

Upgrading from 1.5.* to 2.5.*

Estimated Upgrade Time: 5 - 10 Minutes

Updating Dependencies

Update your czim/laravel-paperclip dependency to ^2.5 in your composer.json file.

	"require": {
		...
		"czim/laravel-paperclip": "^2.5",
		...
	}

Then, in your terminal run:

composer update czim/laravel-paperclip --with-dependencies

In addition, if you are using the czim/file-handling package directly, you should upgrade the package to its ^1,0 release, but be sure to checkout the CHANGELOG

	"require": {
		...
		"czim/file-handling": "^1.0",
		...
	}

Update Configuration

Update your config/paperclip.php file and replace:

        // The base path that the interpolator should use
        'base-path' => ':class/:id_partition/:attribute',

With:

        // The path to the original file to be interpolated. This will also\
        // be used for variant paths if the variant key is unset.
        'original' => ':class/:id_partition/:attribute/:variant/:filename',

        // If the structure for variant filenames should differ from the
        // original, it may be defined here.
        'variant'  => null,

This should now include placeholders to make a full file path including the filename, as opposed to only a directory. Note that this makes the path interpolation logic more in line with the way Stapler handled it.

Contributing

Please see CONTRIBUTING for details.

Credits

License

The MIT License (MIT). Please see License File for more information.

laravel-paperclip's People

Contributors

austenc avatar czim avatar daniel-de-wit avatar dr-de-wit avatar maxgiting avatar miljoen avatar okandas avatar piet-hein avatar ridderarnout avatar wimski avatar wimwidgets avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

laravel-paperclip's Issues

Paperclip storing wrong file on s3 instead of actual file.

In documentation related to the difference with the stapler, there is a line saying

Another difference is that this package does not handle (s3) storage.
All storage is performed through Laravel's storage solution.

So, does paperclip store the files on the s3 bucket?
I have tried this with s3 bucket option with configuration by setting disk => s3 under config/paperclip.php file it is storing the wrong file on the bucket.

Basically I am exporting excel and putting that on s3, but when I am executing the code it is creating excel at storage/app folder (The actual excel which holds the exported data. ) and creating new file under storage/app/public/ which holds the path of the actual exported excel file which is under storage/app and the excel which is created under public folder is being sent to the s3 bucket.

Tracker_2018-11-01_ef22781f-675b-4eae-a0ae-760c8d6aa752.xlsx

Error in Laravel 5.6.37

When I updated Laravel to 5.6.37, paperclip started to throw this error - Object of class Czim\Paperclip\Attachment\Attachment could not be converted to string
On 5.6.35 it works fine.

Laravel 7 getAttributes() memory / loop problem

Hello @czim, and sorry to be the bearer of bad news.

It seems that the "hack" done in this commit will finally need to be implemented in another way:
c429760

Laravel 7 made many changes to where getAttributes() was used so I'm thinking we need another solution to merge paperclip's attributes onto the model.

Any thoughts on how we might accomplish this?

Duplicate record

Hi,

I have an issue with duplicating records for models which have attachements.
I tried
(this was taken from a previous project which used Stapler)

$newImage = new ProductImage;
$newImage->image->setUploadedFile($oldImage->image->path());

But that does not work, as setUploadedFile is not expecting a string as a parameter.
How could I go about copying the file (to the correct path, of course) when duplicating the record?

Thank you.

Define a style to resize only if image is larger than style size

I'm wondering if this library supports my use case. I would like to resize images proportionally down to a width of 1280px only if the original image is larger than 1280px.

As you can see I never found a built-in way to do this using stapler. I had to use a custom closure to get it working.

CodeSleeve/stapler#161

Does laravel-paperclip support this or will I have to use the same custom solution. It would be very nice to simply define the dimensions as 1280> and not have to use a closure.

Any idea?

Allowed memory size and large uploads

Hi there, and thanks for the great plugin!

I'm uploading rather large files (2Gb+) via chunked uploads to Laravel (7). When the file is uploaded and done I'm trying to attach it to the paperclip model like so:

$file = new \Czim\FileHandling\Storage\File\SplFileInfoStorableFile();
$file->setData(new \SplFileInfo($path));
$file->setName("{$uniqueFileName}.{$extension}");
$postAsset->image = $file;
$postAsset->save();

I'm getting the error message:

Allowed memory size of 134217728 bytes exhausted (tried to allocate 2228990984 bytes) in /vendor/czim/file-handling/src/Storage/File/SplFileInfoStorableFile.php on line 60

So if I understand this correctly, to attach this file as it is now I need a "memory_limit" higher than the max file size?

Is there any other way of attaching the file that does not consume this amount of memory? Is it possible to do it in chunks, or append data to the paperclip model as the chunks come in or something similar?

Failed to reprocess attachment

Using latest Homestead I'm getting this error when trying to refresh. Any idea what might be causing it? I see the file it says it can't open in the /tmp directory. The owner/group is vagrant. Would appreciate any ideas on what is causing this.

[2019-07-24 21:13:40] local.ERROR: Failed to reprocess attachment 'logo' of App\Models\Expedition #17: Failed to process variant 'medium': Unable to open image /tmp/filehandling-variant-5d38ca04591ee.jpg {"exception":"[object] (Czim\\Paperclip\\Exceptions\\ReprocessingFailureException(code: 0): Failed to reprocess attachment 'logo' of App\\Models\\Expedition #17: Failed to process variant 'medium': Unable to open image /tmp/filehandling-variant-5d38ca04591ee.jpg at /home/vagrant/sites/biospexProd/vendor/czim/laravel-paperclip/src/Console/Commands/RefreshAttachmentCommand.php:81, Czim\\Paperclip\\Exceptions\\VariantProcessFailureException(code: 0): Failed to process variant 'medium': Unable to open image /tmp/filehandling-variant-5d38ca04591ee.jpg at /home/vagrant/sites/biospexProd/vendor/czim/laravel-paperclip/src/Attachment/Attachment.php:733, Imagine\\Exception\\RuntimeException(code: 0): Unable to open image /tmp/filehandling-variant-5d38ca04591ee.jpg at /home/vagrant/sites/biospexProd/vendor/imagine/imagine/lib/Imagine/Gd/Imagine.php:96)
[stacktrace]
#0 /home/vagrant/sites/biospexProd/vendor/czim/laravel-paperclip/src/Console/Commands/RefreshAttachmentCommand.php(57): Czim\\Paperclip\\Console\\Commands\\RefreshAttachmentCommand->processAttachmentOnModelInstance(Object(App\\Models\\Expedition), Object(Czim\\Paperclip\\Attachment\\Attachment))
#1 [internal function]: Czim\\Paperclip\\Console\\Commands\\RefreshAttachmentCommand->handle()
#2 /home/vagrant/sites/biospexProd/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(32): call_user_func_array(Array, Array)
#3 /home/vagrant/sites/biospexProd/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(90): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#4 /home/vagrant/sites/biospexProd/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(34): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#5 /home/vagrant/sites/biospexProd/vendor/laravel/framework/src/Illuminate/Container/Container.php(576): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#6 /home/vagrant/sites/biospexProd/vendor/laravel/framework/src/Illuminate/Console/Command.php(183): Illuminate\\Container\\Container->call(Array)
#7 /home/vagrant/sites/biospexProd/vendor/symfony/console/Command/Command.php(255): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#8 /home/vagrant/sites/biospexProd/vendor/laravel/framework/src/Illuminate/Console/Command.php(170): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#9 /home/vagrant/sites/biospexProd/vendor/symfony/console/Application.php(921): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#10 /home/vagrant/sites/biospexProd/vendor/symfony/console/Application.php(273): Symfony\\Component\\Console\\Application->doRunCommand(Object(Czim\\Paperclip\\Console\\Commands\\RefreshAttachmentCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#11 /home/vagrant/sites/biospexProd/vendor/symfony/console/Application.php(149): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#12 /home/vagrant/sites/biospexProd/vendor/laravel/framework/src/Illuminate/Console/Application.php(90): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#13 /home/vagrant/sites/biospexProd/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(133): Illuminate\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/vagrant/sites/biospexProd/artisan(37): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 {main}
"} 

Attachment attributes instantiated even when excluded from select statement

Since an upgrade from 2.7 to 3.2, we've been encountering an issue with the new method for attaching the attributes to an entity. If we use a restrictive select on a query to retrieve a subset of attributes, as shown below, any attachments will also be tagged on to the end as soon as the objects are booted and there doesn't seem to be any way to exclude them.

In 2.7 Product::select(['name', 'sku', 'price'])->get() would only retrieve those three attributes for the entity as expected. Since the update (and presumably the switch to utilising model events) any attachment attributes will also be initialised.

This causes issues with code that serialises or converts entities as it can get stuck in a recursive loop following the attachment's $instance property back to the entity itself. We encountered this issue with Laravel Datatabes after updating Paperclip. Specifically, this function in the Datatables package gets stuck recursively looping between the entity, an attachment and the $instance reference back to the entity.

Using removeFileAttributes() after retrieving the entities would likely work, but this isn't possible with packages that only take in a query object and handles the retrieval itself.

CLI option to (re)generate only one resize of a specific attachment

Hi!

Would it be possible to have an option to only refresh a single resize of an attachment? Let's say I have this model App\Models\MyModel that already has one attachment "image" and 2 existing resizes "resize-1" and "resize-2".

If I now add a new resize "resize-3" to my model config it would be nice to be able to only generate the images for the newly added resize, whereas php artisan paperclip:refresh "App\Models\MyModel" --attachments image seems to regenerate all resizes for attachment "image".

I'd imagine a CLI option "--attachment" (singular, so that there can be no ambiguity regarding which attachment we're talking about). Then an additional option "--resizes" could take the list of resizes to refresh:

$ php artisan paperclip:refresh "App\Models\MyModel" --attachment image --resizes resize-3

Is this a viable idea? Or does this already exist in some way, and am I missing something?

Thanks!

Saving file from URL not working correctly

I'm trying to follow the instructions in the README for attaching a file from a URL, but it's not working correctly. It says:

Stapler would automatically resolve any string URL assigned to the Model's attachment attribute and download and store the response to that URL. Paperclip does not do this automatically.
If you wish to store the contents of a URL, you have some options. You can use the Czim\FileHandling\Storage\File\StorableFileFactory@makeFromUrl method and its return value.

My code looks like this:

$file = app(Czim\FileHandling\Storage\File\StorableFileFactory::class)->makeFromUrl($url);
$document = new App\ProductDocument;
$document->document = $file;
$document->save();

I'm getting the following error:

UnexpectedValueException with message 'Could not interpret given data, string value expected'

It's being thrown from Line 91 in vendor/czim/file-handling/src/Storage/File/StorableFileFactory.php.

Am I doing something wrong or is there a bug? I have confirmed that the file is being correctly downloaded. $file is an instance of Czim\FileHandling\Storage\File\SplFileInfoStorableFile. However, something goes wrong when trying to save the $document.

How I can change filename?

I want use a hash or maybe model slug for file name.
How I can change filename of uploaded file?

In config I use:
'base-path' => ':class_name/:attribute',

Re-uploading of image attachment with variants - variants have incorrect extension

I added variants to my model's paperclip config and encountered a problem when re-uploading images. Original was uploaded as expected, but variants were uploaded with incorrect extension (from previous version).

Example: Firstly I uploaded image image.png and everything was good. Original and variants were uploaded and saved.
Later I decided to replace image to image.jpg and original was uploaded correctly, but variants had incorrect extension png from previous version.
I guess problem in methods variantFilename() of AttachmentData and Attachment classes. They return different results after interpolation. AttachmentData class returns path based on previous version of attachment, even in case of re-uploading.

Config:

        $this->hasAttachedFile('logo', [
            'storage' => 's3',
            'variants' => [
                Variant::make('thumb')->steps([
                    ResizeStep::make()->square(100)
                ]),
                Variant::make('small')->steps([
                    ResizeStep::make()->square(300)
                ]),
            ],
            'attributes' => [
                'variants' => true,
                'file_size' => false,
                'content_type' => false
            ]
        ]);

Path generation

A bit confused and tried various things.... I see you removed url() with 2.5 so what is called to generate the file path? $project->document->????

Is there a method to get only the relative path?

I need to make some files private in order to authorize the user first before downloading them.

Is there a method to get only the file relative path (without the app URL), without changing the paperclip.storage.base-urls config?

Thanks in advance.

Improve documentation

The documentation has some flaws and it shouldn't depend on Stapler's documentation anymore. It will need to be made complete, and provide clear instructions for those that have no experience with Stapler.

Is there a way to timeout remote URL fetches?

I'm getting some really long requests from a specific provider, and it's causing me some headaches.

The offending line where the request hangs is here:
Czim\FileHandling\Support\Download\UrlDownloader::downloadToTempLocalPath

Is there any way in laravel-paperclip to set a timeout on UrlDownloads?

Processed variant ignores Orientation EXIF

Problem: upload an image with correct orientation EXIF data -> the processed variant orientation is not applied.

Model Photo.php:

    public function __construct(array $attributes = [])
    {
        $this->hasAttachedFile('image', [
            'variants'  => [
                Variant::make('thumb')->steps([
                    AutoOrientStep::make(),
                    ResizeStep::make()->width(480)->height(270)->crop(), // = '480x270#'
                ]),
                Variant::make('small')->steps([
                    AutoOrientStep::make(),
                    ResizeStep::make()->width(480), // = '480x'
                ]),
                Variant::make('medium')->steps([
                    AutoOrientStep::make(),
                    ResizeStep::make()->width(640), // = '640x'
                ]),
                Variant::make('large')->steps([
                    AutoOrientStep::make(),
                    ResizeStep::make()->width(1280), // = '1280x'
                ]),
            ],
            'url' => config('services.cdn.image_not_found'),
        ]);

        parent::__construct($attributes);
    }

Problematic photos:

using a meta tag tool i was able to verify these photos had the correct orientation EXIF data.

Is there a possibility that this is a bug?

Filename encoding inconsistent with stapler

I'm migrating from stapler and my last hurdle is that filenames are being encoded differently.

Stapler would save the filename without encoding.

As a work around i've extended the Interpolator to override the filename function, doing a urldecode on $attachment->originalFilename().

Would love some feedback, is this something you'd like to have configurable? Is there a better solution? Would be happy to contrib.

URL not being generated correctly.

Using the following path:

'path' => 'Foo/:attachment/:id/:variant/:hash/',

creates the following url:

/Foo/:attachment/:id/original/:hash/:variant/filename.filextention

It seems that an additional original is being appended, no matter what the variant (and where it is not given in the path).

I'll look into this further, but just leaving my findings here.

Migration guide?

Hey @czim I'm interested in using this package instead of Stapler, but we already use Stapler (via laravel-stapler) in our project. I saw the list of differences in the readme, but was wondering if there was a more robust migration guide?

If not, is there anything else I should be aware of when attempting the transition? Is the rest of the API mostly the same as stapler's?

This looks far more compatible (sick of battling this every upgrade!), so I'd love to give it a shot! Thanks!

Strategy for changing interpolation

If we have decided to change our interpolation mechanism - for example:

  'url' => '/:class_name/:id/:attachment/:style/:hash.:extension',

becomes

  'url' => '/:class_name/:id/:attachment/:style/:secure_hash.:extension',

How can we change this without breaking all of our existing assets, is there a predetermined migration path here?

Serializing models with attachment (for Redis)

I'm currently in the process of replacing Stapler with Paperclip on a site. Getting it working at a basic level has been pretty painless, with the old images being displayed correctly by Paperclip without much configuration. However with Paperclip installed, any attempt to serialize a model (for example using Cache::rememberForever) fails, with the error "Serialization of 'Closure' is not allowed".

I noticed that the attachedFiles property includes the Laravel container, and this contains the only closures I can find on this model, under attachedFiles > handler > processor > stategyFactory > container.

Is there a way to get around this that I'm missing? Thanks.

A method to check if the file is set?

Is there an official way to check if the attachment was set?

$myModel->attachment is always set, even if the file does not exists. It would be cool something like $myModel->attachment->exists().

Since I can't find it, I'm using !$myModel->attachment->size(), but it's not very good, since it fails with 0-sized files

attaching via url with url encoded filenames is problematic

this will not work because the url is interpreted as raw datatype instead of uri:

$photo->image = "https://my-domain.com/filename_with space.jpeg";
$photo->save();

so i tried the following:

$filename = urlencode("filename_with space.jpeg");
$photo->image = "https://my-domain.com/${$filename}";
$photo->save();

which uploads to S3 fine, but then saves the filename with encoded filename. this is problematic because S3 will not recognize this resource.

Manually able to trigger rotation on images

I briefly looked through czim/file-handler and czim/laravel-paperclip but did not find a convenient way to trigger a rotation on attachments.

  1. Is this even possible for me to extend and open a PR for it?

  2. or should I download, rotate and update the model attachment?

any guidance / starting point on the matter is greatly appreciated.

File goes missing at times after upload

At times while uploading the file goes missing and an exception is thrown. This only happens sometimes and I cannot replicate error on will.

Error log says
(Symfony\\Component\\HttpFoundation\\File\\Exception\\FileNotFoundException(code: 0): The file \"/root/public\" does not exist at /root/vendor/symfony/http-foundation/File/MimeType/MimeTypeGuesser.php:116)

Stack Trace

#0 /root/vendor/czim/file-handling/src/Support/Content/MimeTypeHelper.php(37): Symfony\\Component\\HttpFoundation\\File\\MimeType\\MimeTypeGuesser->guess('/root/w...')
#1 /root/vendor/czim/file-handling/src/Storage/File/StorableFileFactory.php(127): Czim\\FileHandling\\Support\\Content\\MimeTypeHelper->guessMimeTypeForPath('/root/w...')
#2 /root/vendor/czim/file-handling/src/Storage/File/StorableFileFactory.php(94): Czim\\FileHandling\\Storage\\File\\StorableFileFactory->makeFromFileInfo(Object(Illuminate\\Http\\UploadedFile), 'D458D0B1-9F2D-4...', NULL)
#3 /root/vendor/czim/laravel-paperclip/src/Model/PaperclipTrait.php(129): Czim\\FileHandling\\Storage\\File\\StorableFileFactory->makeFromAny(Object(Illuminate\\Http\\UploadedFile))
#4 /root/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php(1394): App\\Vendors\\VendorGalleryMedia->setAttribute('vendor_image', Object(Illuminate\\Http\\UploadedFile))
#5 /root/app/Http/Controllers/Admin/VendorImagesController.php(115): Illuminate\\Database\\Eloquent\\Model->__set('vendor_image', Object(Illuminate\\Http\\UploadedFile))
#6 [internal function]: App\\Http\\Controllers\\Admin\\VendorImagesController->update(Object(Illuminate\\Http\\Request), '400')

Filesystems.php settings are as below

local' => [
            'driver' => 'local',
            'root'   => storage_path('app'),
        ],
        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'url' => env('APP_URL').'/storage',
            'visibility' => 'public',
        ],
        'upload' => [
            'driver' => 'local',
            'root' => public_path('uploads'),
            'visibility' => 'public',
        ],
        'backups' => [
			'driver' => 'local',
			'root' => storage_path('backups'), 
		],
        
      's3' => [
            'driver' => 's3',
            'key'    => env('S3_APP_KEY'),
            'secret' => env('S3_SECRET_KEY'),
            'region' => env('S3_REGION'),
            'bucket' => env('S3_BUCKET'),
            'visibility' => 'public',
            'options' => [
                'CacheControl' => 'max-age=315360000, no-transform, public',
            ],
        ]

Laravel 7

Hello,

Will this package be compatible Laravel 7.x ? Any estimated delivery date ?

Thanks

Model returned with everything

When querying for a result on a simple 4 column model using an attachment, then dumping it using dd(), it dumps everything in the application. By everything I mean lines of providers, artisan commands, every class in the application, etc. I was wondering if that's normal. If I dump a result from a model without Paperclip, I simply get the usual model information. Mainly, I'm concerned about how this might effect speeds.

#attachedFiles: array:1 [
    "download" => Czim\Paperclip\Attachment\Attachment {#1258
      #instance: App\Models\ProjectResource {#1257}
      #name: "download"
      #handler: Czim\FileHandling\Handler\FileHandler {#1260
        #storage: Czim\FileHandling\Storage\Laravel\LaravelStorage {#1262
          #filesystem: Illuminate\Filesystem\FilesystemAdapter {#1137
            #driver: League\Flysystem\Filesystem {#1135
              #adapter: League\Flysystem\Adapter\Local {#1134
                #pathSeparator: "/"
                #permissionMap: array:2 [
                  "file" => array:2 [
                    "public" => 420
                    "private" => 384
                  ]
                  "dir" => array:2 [
                    "public" => 493
                    "private" => 448
                  ]
                ]
                #writeFlags: 2
                -linkHandling: 2
                #pathPrefix: "/home/vagrant/sites/biospexDev/storage/app/public/"
              }
              #plugins: []
              #config: League\Flysystem\Config {#1136
                #settings: array:2 [
                  "url" => "https://dev.biospex.test/storage"
                  "visibility" => "public"
                ]
                #fallback: null
              }
            }
          }
          #isLocal: false
          #baseUrl: "https://dev.biospex.test/storage"
        }
        #processor: Czim\FileHandling\Variant\VariantProcessor {#1144
          #fileFactory: Czim\FileHandling\Storage\File\StorableFileFactory {#1151
            #mimeTypeHelper: Czim\FileHandling\Support\Content\MimeTypeHelper {#1148}
            #interpreter: Czim\FileHandling\Support\Content\UploadedContentInterpreter {#1149}
            #downloader: Czim\FileHandling\Support\Download\UrlDownloader {#1152
              #mimeTypeHelper: Czim\FileHandling\Support\Content\MimeTypeHelper {#1148}
            }
            #markNextUploaded: false
          }
          #strategyFactory: Czim\FileHandling\Variant\VariantStrategyFactory {#1146
            #container: Czim\FileHandling\Support\Container\LaravelContainerDecorator {#1145
              -container: Illuminate\Foundation\Application {#2
                #basePath: "/home/vagrant/sites/biospexDev"
                #hasBeenBootstrapped: true
                #booted: true
                #bootingCallbacks: array:5 [
                  0 => Closure {#262
                    class: "Illuminate\Foundation\Application"
                    this: Illuminate\Foundation\Application {#2}
                    use: { …1}
                    file: "./vendor/laravel/framework/src/Illuminate/Foundation/Application.php"
                    line: "710 to 712"
                  }
                  1 => Closure {#268
                    class: "Illuminate\Foundation\Application"
                    this: Illuminate\Foundation\Application {#2}
                    use: { …1}
                    file: "./vendor/laravel/framework/src/Illuminate/Foundation/Application.php"
                    line: "710 to 712"
                  }
                  2 => Closure {#306
                    class: "Illuminate\Foundation\Application"
                    this: Illuminate\Foundation\Application {#2}
                    use: { …1}
                    file: "./vendor/laravel/framework/src/Illuminate/Foundation/Application.php"
                    line: "710 to 712"
                  }
                  3 => Closure {#314
                    class: "Illuminate\Foundation\Application"
                    this: Illuminate\Foundation\Application {#2}
                    use: { …1}
                    file: "./vendor/laravel/framework/src/Illuminate/Foundation/Application.php"
                    line: "710 to 712"
                  }
                  4 => Closure {#490
                    class: "Illuminate\Foundation\Application"
                    this: Illuminate\Foundation\Application {#2}
                    use: { …1}
                    file: "./vendor/laravel/framework/src/Illuminate/Foundation/Application.php"
                    line: "710 to 712"
                  }
                ]
                #bootedCallbacks: array:3 [
                  0 => Closure {#28
                    class: "Illuminate\Foundation\Console\Kernel"
                    this: App\Console\Kernel {#27
                      #commands: []
                      #app: Illuminate\Foundation\Application {#2}
                      #events: Illuminate\Events\Dispatcher {#25
                        #container: Illuminate\Foundation\Application {#2}
                        #listeners: array:32 [ …32]
                        #wildcards: []
                        #wildcardsCache: array:44 [ …44]
                        #queueResolver: Closure {#26 …5}
                      }
                      #artisan: Illuminate\Console\Application {#626 …18}
                      #commandsLoaded: true
                      #bootstrappers: array:7 [ …7]
                    }
                    file: "./vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php"
                    line: "89 to 91"
                  }
                  1 => Closure {#276
                    class: "Msurguy\Honeypot\HoneypotServiceProvider"
                    this: Msurguy\Honeypot\HoneypotServiceProvider {#161
                      #defer: false
                      #app: Illuminate\Foundation\Application {#2}
                    }
                    parameters: { …1}
                    file: "./vendor/msurguy/honeypot/src/Msurguy/Honeypot/HoneypotServiceProvider.php"
                    line: "45 to 55"
                  }
                  2 => Closure {#340
                    class: "Illuminate\Foundation\Support\Providers\RouteServiceProvider"
                    this: App\Providers\RouteServiceProvider {#184
                      #namespace: "App\Http\Controllers"
                      #app: Illuminate\Foundation\Application {#2}
                      #defer: false
                    }
                    file: "./vendor/laravel/framework/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php"
                    line: "38 to 41"
                  }
                ]
                #terminatingCallbacks: []
                #serviceProviders: array:54 [
                  0 => Illuminate\Events\EventServiceProvider {#6
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }
                  1 => Illuminate\Log\LogServiceProvider {#8
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }
                  2 => Illuminate\Routing\RoutingServiceProvider {#10
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }
                  3 => Illuminate\Auth\AuthServiceProvider {#49
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }
                  4 => Illuminate\Cookie\CookieServiceProvider {#60
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }
                  5 => Illuminate\Database\DatabaseServiceProvider {#62
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }
                  6 => Illuminate\Encryption\EncryptionServiceProvider {#69
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }
                  7 => Illuminate\Filesystem\FilesystemServiceProvider {#71
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }
                  8 => Illuminate\Foundation\Providers\FormRequestServiceProvider {#77
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }
                  9 => Illuminate\Foundation\Providers\FoundationServiceProvider {#76
                    #providers: array:1 [ …1]
                    #instances: array:1 [ …1]
                    #app: Illuminate\Foundation\Application {#2}
                    #defer: false
                  }

Variants not being created

I'm running Laravel 5.6 on my local Mac that runs High Sierra. I have defined the following variants on my model:

public function __construct(array $attributes = [])
{
	$this->hasAttachedFile('image', [
		'variants' => [
			'thumbnail' => [
				'auto-orient' => [],
				'resize' => ['dimensions' => '300'],
			],
			'zoom' => [
				'auto-orient' => [],
				'resize' => ['dimensions' => '1280'],
			],
		],
		'attributes' => [
			'variants' => true
		]
	]);

	parent::__construct($attributes);
}

I'm running the following commands in Laravel's tinker prompt:

$photo = App\CustomerPhoto::first();
$photo->image = File::get('/Users/me/Desktop/placeholder.jpg');
$photo->save();

The result is an error that says:

Czim/FileHandling/Exceptions/CouldNotProcessDataException with message 'Failed to make variant copy to '/var/folders/6l/7z4kdwpj3fj0bl535ztfmpgw0000gn/T/filehandling-variant-5aea9727a7fdb.jpeg''

The original file was correctly created in the following location: storage/app/public/App/CustomerPhoto/000/000/001/image/original/57df9e4e46daa5c3.jpeg

However, as you can see from the error message, no variants were created. What would be causing this?

Option for default variants

Next to have a fallback variant configuration it might be nice to have default variant options.

For example, in my project I would like to have thumbnails of a standard size.

AutoOrientStep not working in heroku

For the past 3 days, I have been stuck with a rather frustrating problem. AutoOrientStep works on local but not on any heroku deployed servers.

Here are all the things I made sure to do:

  • locally mimic heroku environment (copy env vars + use heroku local)
  • composer cache to be cleared using composer cacheclear before reinstalling all composer packages
  • ensure composer package versions are identical locally and remotely
  • ensure php versions are identical locally and remotely

Suspicious things:

  • no failures logged on heroku servers.
  • identical commit, identical configurations launched on an EC2 instance works as expected
  • ResizeStep works. only AutoOrientStep does not.

This is not likely something to do with the library, but some configuration issue. i would like to document this in case anyone else has experienced this.

I have exhausted my debugging options. Any debugging suggestions would be appreciated.

Photo Model:

...
public function __construct(array $attributes = [])
{
    $this->hasAttachedFile('image', [
        'variants'  => [
            Variant::make('thumb')->steps([
                AutoOrientStep::make(),
                ResizeStep::make()->width(480)->height(270)->crop(), // = '480x270#'
            ]),
            Variant::make('small')->steps([
                AutoOrientStep::make(),
                ResizeStep::make()->width(480), // = '480x'
            ]),
            Variant::make('medium')->steps([
                AutoOrientStep::make(),
                ResizeStep::make()->width(640), // = '640x'
            ]),
            Variant::make('large')->steps([
                AutoOrientStep::make(),
                ResizeStep::make()->width(1280), // = '1280x'
            ]),
            Variant::make('xlarge')->steps([
                AutoOrientStep::make(),
                ResizeStep::make()->width(2560), // = '2560x'
            ]),
        ],
        'url' => config('services.cdn.image_not_found'),
    ]);

    parent::__construct($attributes);
}
...

A Queued Laravel Job:

...
// process variants
$file = app(StorableFileFactory::class)->makeFromUrl($fileUrl);
$photo->image = $file;
$photo->save();
...

Variant data not being written to database

I'm not sure what I'm missing, but based on the documentation it seemed that if I have an attachment named image, I could add a column called image_variants, and that should be populated with JSON data about the attachments that were created. Here is my setup:

$this->hasAttachedFile('image', [
	'variants' => [
		'thumbnail' => [
			'auto-orient' => [],
			'resize' => ['dimensions' => '300', 'convert_options' => ['quality' => 80]],
		],
		'zoom' => [
			'auto-orient' => [],
			'resize' => ['dimensions' => '1280'],
		],
	],
	'attributes' => [
		'variants' => true
	]
]);

You can see I am including the attributes.variants key and setting it true. However, after saving, the file is put on s3 along with the variants, all the other database fields are populated, but the image_variants field is simply [].

What am I doing wrong here?

Allow setting fallback attachment in model configuration

It would be nice to be able to set a fallback for when no attachment is set. url() should then return the fallback URL.

This should be optional and default to the current functionality when not configured.

This needs to allow for checking whether the returned URL is for the fallback or a 'real' attachment.

How to make use of the hooks to optimise images.

I've had a look at the hook functionality and the interface that goes with it but I'm not exactly sure which public method can be used to access the underlying file in the storage?

I was hoping to be able to link paperclip with this https://github.com/spatie/laravel-image-optimizer
so that the original image file is saved, but after resizing variants they are optimised (or ideally doing it all in one go).

Any help would be much appreciated =)

InvalidArgumentException with message 'Height value for portrait resize is empty. This may be caused by unfixed EXIF-rotated images'

I'm receiving the following error trying to save a photo with variants:

InvalidArgumentException with message 'Height value for portrait resize is empty. This may be caused by unfixed EXIF-rotated images'

This is my model set up:

$this->hasAttachedFile('image', [
	'variants' => [
			Variant::make('thumbnail')->steps([
					AutoOrientStep::make(),
					ResizeStep::make()->width(300),
			]),

			Variant::make('zoom')->steps([
					AutoOrientStep::make(),
					ResizeStep::make()->width(1280)->convertOptions(['quality' => 100]),
			]),
	],
]);

This is the image I am trying to save with variants when I receive the error:

https://s3.amazonaws.com/storeyourboard/App/CustomerPhoto/000/000/014/image/original/image_82432486101421854469.jpg

I have no clue what this error means. Can you explain?

Big imports memory problem

The library gets stuck when i'm trying to import a lot of images. I tried to use gc_collect_cycles() and allocate more memory from php.ini file memory_limit=1024M and it seems to work better, but at high date volume it is still not responding.

  • If you need logs or something else i can provide it.

Feature: attachment-level storage visibility configuration

Hi @czim,

tl;dr: I want to add an attachment-level configuration option to specify the visibility level of the stored file. This is currently not possible, correct? Any tips for the implementation? You don't happen to be working on this already?

The company I work for uses codesleeve/laravel-stapler for working with file attachments in multiple applications. Since this package is no longer maintained, as opposed to yours, we wish to start using czim/laravel-paperclip. However, our application requires some files to be publicly accessible (e.g. company content images), but others not (e.g. a user's personal information / resume).
Unless I missed something in the documentation, it seems to me that the only place where the visibility level of files can be specified currently is Laravel's config/filesystems.disks array. This won't do for us, since we need to be able to specify the visibility level per attachment, instead of globally.
Therefore, I suggest adding a visibility => 'public' || 'private' key to the $options argument of the PaperclipTrait::hasAttachedFile method. This option then has to find it's way all the way down to Czim\FileHandling\Storage\Laravel\LaravelStorage::store where it can be added as a third argument to $this->filesystem->put($path, $file->content()[, $visibility]).
Since I believe this new feature would involve making changes to two different packages (czim/laravel-paperclip and czim/file-handling), that support a variety of attachment configuration options (default object-oriented configuration and legacy stapler array configuration), it would be much appreciated if you could give me some pointers for the minimum requirements to make this work, pitfalls and restrictions or, even better, collaborate on it with me.

I hope to hear from you soon.

Kind regards,

yoeri

Using outside of laravel

Now paperclip works in lumen with adding several global helpers.
Is it possible to make it working without laravel, just with Capsule?
Generally, it is eloquent (database) + filesystems, It should not depends on whole laravel.

Default URL

Hi,

Laravel Stapler had a config key for the default_url,

        $this->hasAttachedFile('foo', [
            'styles' => [
                'thumbnail' => '100x100',
                'large' => '300x300'
            ],
            'url' => '/system/:attachment/:id_partition/:style/:filename',
            'default_url' => '/:attachment/:style/missing.jpg'
        ]);

which was returned if no image was found. I tried it in Paperclip also, but it doesn't seem to work. Is there a way to set this up, maybe on a model basis?

base URL should not be necessary for s3 storage

Using Laravel 5.6, I was finding that I needed to include my bucket name in the AWS_URL environment in order for the Storage facade to generate the correct URLs. For example, if:

AWS_URL=https://s3.amazonaws.com
AWS_BUCKET=mybucket

The resulting URL from Storage::url('file.txt'), will not include the bucket name:

https://s3.amazonaws.com/file.txt

If I hardcode the bucket name to the url, it works:

AWS_URL=https://s3.amazonaws.com/mybucket

But it seems like I shouldn't have to do that. So, another option that works, as seen on this thread, is to completely remove AWS_URL from the config. This works and results in Storage::url('file.txt') returning the subdomain version of the url, which is fine, correctly including the bucket name:

https://mybucket.s3.amazonaws.com/file.txt

The problem is, this set up results in paperclip throwing an error:

RuntimeException with message 'Could not determine base URL for storage disk 'paperclip'

Is there any way to fix this and make the two settings play nicely together?

Saving new Image from url causes Exception

My Saving Code

$order_image = ModelName::create( [
     'order_id' => $object->order_id,      
     'image' => $url_string,
     'default' => $object->default,
     'visible' => $object->visible,
     'priority' => $object->priority
] );

Screenshot from 2020-05-11 12-22-08

Image file with variants issue for URL without file extension

Storing an image file from an URL that has no file extension may result in an Imagine\Exception\InvalidArgumentException being thrown, with the message Saving image in "" format is not supported, please use one of the following extensions.

This occurs because imagine uses the file name to determine the type, instead of deriving the mime type. This may require altering czim/file-handling, or a config setting may be added to enforce file extensions based on mimetype, where omitted.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.