1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 12:12:48 +01:00

Improved handling of various document types; better documents zip

This commit is contained in:
Joshua Dwire 2016-03-24 11:33:28 -04:00
parent e6056104bd
commit 1c5c45a1e1
11 changed files with 183 additions and 117 deletions

View File

@ -46,7 +46,7 @@ class DocumentController extends BaseController
if($stream){
$headers = [
'Content-Type' => $document->type,
'Content-Type' => Document::$types[$document->type]['mime'],
'Content-Length' => $document->size,
];
@ -56,7 +56,7 @@ class DocumentController extends BaseController
}
else{
$response = Response::make($document->getRaw(), 200);
$response->header('content-type', $document->type);
$response->header('content-type', Document::$types[$document->type]['mime']);
}
return $response;
@ -80,9 +80,9 @@ class DocumentController extends BaseController
return redirect($direct_url);
}
$extension = pathinfo($document->preview, PATHINFO_EXTENSION);
$previewType = pathinfo($document->preview, PATHINFO_EXTENSION);
$response = Response::make($document->getRawPreview(), 200);
$response->header('content-type', Document::$extensions[$extension]);
$response->header('content-type', Document::$types[$previewType]['mime']);
return $response;
}
@ -99,7 +99,7 @@ class DocumentController extends BaseController
return $response;
}
if(substr($document->type, 0, 6) != 'image/'){
if(!$document->isPDFEmbeddable()){
return Response::view('error', array('error'=>'Image does not exist!'), 404);
}

View File

@ -19,8 +19,7 @@ use App\Ninja\Repositories\ActivityRepository;
use App\Events\InvoiceInvitationWasViewed;
use App\Events\QuoteInvitationWasViewed;
use App\Services\PaymentService;
use League\Flysystem\Filesystem;
use League\Flysystem\ZipArchive\ZipArchiveAdapter;
use Barracuda\ArchiveStream\ZipArchive;
class PublicClientController extends BaseController
{
@ -138,8 +137,13 @@ class PublicClientController extends BaseController
'phantomjs' => Input::has('phantomjs'),
);
if($account->isPro() && $this->canCreateInvoiceDocsZip($invoice)){
$data['documentsZipURL'] = URL::to("client/documents/{$invitation->invitation_key}");
if($account->isPro() && $this->canCreateZip()){
$zipDocs = $this->getInvoiceZipDocuments($invoice, $size);
if(count($zipDocs) > 1){
$data['documentsZipURL'] = URL::to("client/documents/{$invitation->invitation_key}");
$data['documentsZipSize'] = $size;
}
}
return View::make('invoices.view', $data);
@ -173,6 +177,12 @@ class PublicClientController extends BaseController
return $paymentTypes;
}
protected function humanFilesize($bytes, $decimals = 2) {
$size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB');
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$size[$factor];
}
public function download($invitationKey)
{
if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
@ -391,7 +401,7 @@ class PublicClientController extends BaseController
$document = Document::scope($publicId, $invitation->account_id)->first();
if(!$document || substr($document->type, 0, 6) != 'image/'){
if(!$document->isPDFEmbeddable()){
return Response::view('error', array('error'=>'Image does not exist!'), 404);
}
@ -423,19 +433,43 @@ class PublicClientController extends BaseController
return function_exists('gmp_init');
}
protected function canCreateInvoiceDocsZip($invoice){
if(!$this->canCreateZip())return false;
if(count($invoice->documents) == 1)return false;
protected function getInvoiceZipDocuments($invoice, &$size=0){
$documents = $invoice->documents->sortBy('size');
$size = 0;
$maxSize = MAX_ZIP_DOCUMENTS_SIZE * 1000;
$i = 0;
foreach($invoice->documents as $document){
if($document->size <= $maxSize)$i++;
if($i > 1){
return true;
$toZip = array();
foreach($documents as $document){
if($size + $document->size > $maxSize)break;
if(!empty($toZip[$document->name])){
// This name is taken
if($toZip[$document->name]->hash != $document->hash){
// 2 different files with the same name
$nameInfo = pathinfo($document->name);
for($i = 1;; $i++){
$name = $nameInfo['filename'].' ('.$i.').'.$nameInfo['extension'];
if(empty($toZip[$name])){
$toZip[$name] = $document;
$size += $document->size;
break;
} else if ($toZip[$name]->hash == $document->hash){
// We're not adding this after all
break;
}
}
}
}
else{
$toZip[$document->name] = $document;
$size += $document->size;
}
}
return false;
return $toZip;
}
public function getInvoiceDocumentsZip($invitationKey){
@ -451,64 +485,24 @@ class PublicClientController extends BaseController
return Response::view('error', array('error'=>'No documents'), 404);
}
$documents = $invoice->documents->sortBy('size');
$size = 0;
$maxSize = MAX_ZIP_DOCUMENTS_SIZE * 1000;
$toZip = array();
foreach($documents as $document){
$size += $document->size;
if($size > $maxSize)break;
if(!empty($toZip[$document->name])){
$hasSameHash = false;
foreach($toZip[$document->name] as $sameName){
if($sameName->hash == $document->hash){
$hasSameHash = true;
break;
}
}
if(!$hasSameHash){
// 2 different files with the same name
$toZip[$document->name][] = $document;
}
else{
// We're not adding this after all
$size -= $document->size;
}
}
else{
$toZip[$document->name] = array($document);
}
}
$toZip = $this->getInvoiceZipDocuments($invoice);
if(!count($toZip)){
return Response::view('error', array('error'=>'No documents small enough'), 404);
}
$zip = new \Barracuda\ArchiveStream\ZipArchive($invitation->account->name.' Invoice '.$invoice->invoice_number.'.zip');
$zip = new ZipArchive($invitation->account->name.' Invoice '.$invoice->invoice_number.'.zip');
return Response::stream(function() use ($toZip, $zip) {
foreach($toZip as $documentsSameName){
$i = 0;
foreach($documentsSameName as $document){
$name = $document->name;
if($i){
$nameInfo = pathinfo($document->name);
$name = $nameInfo['filename'].' ('.$i.').'.$nameInfo['extension'];
}
$fileStream = $document->getStream();
if($fileStream){
$zip->init_file_stream_transfer($name, $document->size);
while ($buffer = fread($fileStream, 8192))$zip->stream_file_part($buffer);
fclose($fileStream);
$zip->complete_file_stream();
}
else{
$zip->add_file($name, $document->getRaw());
}
foreach($toZip as $name=>$document){
$fileStream = $document->getStream();
if($fileStream){
$zip->init_file_stream_transfer($name, $document->size, array('time'=>$document->created_at->timestamp));
while ($buffer = fread($fileStream, 256000))$zip->stream_file_part($buffer);
fclose($fileStream);
$zip->complete_file_stream();
}
else{
$zip->add_file($name, $document->getRaw());
}
}
$zip->finish();

View File

@ -5,31 +5,68 @@ use DB;
class Document extends EntityModel
{
public static $extensions = array(
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'pdf' => 'application/pdf',
'gif' => 'image/gif'
public static $extraExtensions = array(
'jpg' => 'jpeg',
'tif' => 'tiff',
);
public static $allowedMimes = array(// Used by Dropzone.js; does not affect what the server accepts
'image/png', 'image/jpeg', 'image/tiff', 'application/pdf', 'image/gif', 'image/vnd.adobe.photoshop', 'text/plain',
'application/zip', 'application/msword',
'application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','application/postscript', 'image/svg+xml',
'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.ms-powerpoint',
);
public static $types = array(
'image/png' => array(
'extension' => 'png',
'png' => array(
'mime' => 'image/png',
),
'image/jpeg' => array(
'extension' => 'jpeg',
'ai' => array(
'mime' => 'application/postscript',
),
'image/tiff' => array(
'extension' => 'tiff',
'svg' => array(
'mime' => 'image/svg+xml',
),
'image/gif' => array(
'extension' => 'gif',
'jpeg' => array(
'mime' => 'image/jpeg',
),
'application/pdf' => array(
'extension' => 'pdf',
'tiff' => array(
'mime' => 'image/tiff',
),
'pdf' => array(
'mime' => 'application/pdf',
),
'gif' => array(
'mime' => 'image/gif',
),
'psd' => array(
'mime' => 'image/vnd.adobe.photoshop',
),
'txt' => array(
'mime' => 'text/plain',
),
'zip' => array(
'mime' => 'application/zip',
),
'doc' => array(
'mime' => 'application/msword',
),
'xls' => array(
'mime' => 'application/vnd.ms-excel',
),
'ppt' => array(
'mime' => 'application/vnd.ms-powerpoint',
),
'xlsx' => array(
'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
),
'docx' => array(
'mime' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
),
'pptx' => array(
'mime' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
),
);
@ -142,11 +179,17 @@ class Document extends EntityModel
return url('client/document/'.$invitation->invitation_key.'/'.$this->public_id.'/'.$this->name);
}
public function isPDFEmbeddable(){
return $this->type == 'jpeg' || $this->type == 'png' || $this->preview;
}
public function getVFSJSUrl(){
if(!$this->isPDFEmbeddable())return null;
return url('document/js/'.$this->public_id.'/'.$this->name.'.js');
}
public function getClientVFSJSUrl(){
if(!$this->isPDFEmbeddable())return null;
return url('client/document/js/'.$this->public_id.'/'.$this->name.'.js');
}

View File

@ -61,14 +61,22 @@ class DocumentRepository extends BaseRepository
$uploaded = $input['file'];
$extension = strtolower($uploaded->extension());
if(empty(Document::$extensions[$extension])){
if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){
$documentType = Document::$extraExtensions[$extension];
}
else{
$documentType = $extension;
}
if(empty(Document::$types[$documentType])){
return Response::json([
'error' => 'Unsupported extension',
'code' => 400
], 400);
}
$documentType = Document::$extensions[$extension];
$documentTypeData = Document::$types[$documentType];
$filePath = $uploaded->path();
$name = $uploaded->getClientOriginalName();
$size = filesize($filePath);
@ -80,10 +88,10 @@ class DocumentRepository extends BaseRepository
], 400);
}
$documentTypeData = Document::$types[$documentType];
$hash = sha1_file($filePath);
$filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentTypeData['extension'];
$filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType;
$document = Document::createNew();
$disk = $document->getDisk();
@ -94,20 +102,20 @@ class DocumentRepository extends BaseRepository
}
// This is an image; check if we need to create a preview
if(in_array($documentType, array('image/jpeg','image/png','image/gif','image/bmp','image/tiff'))){
if(in_array($documentType, array('jpeg','png','gif','bmp','tiff','psd'))){
$makePreview = false;
$imageSize = getimagesize($filePath);
$width = $imageSize[0];
$height = $imageSize[1];
$imgManagerConfig = array();
if(in_array($documentType, array('image/gif','image/bmp','image/tiff'))){
if(in_array($documentType, array('gif','bmp','tiff','psd'))){
// Needs to be converted
$makePreview = true;
} else if($width > DOCUMENT_PREVIEW_SIZE || $height > DOCUMENT_PREVIEW_SIZE){
$makePreview = true;
}
if($documentType == 'image/bmp' || $documentType == 'image/tiff'){
if(in_array($documentType,array('bmp','tiff','psd'))){
if(!class_exists('Imagick')){
// Cant't read this
$makePreview = false;
@ -117,13 +125,13 @@ class DocumentRepository extends BaseRepository
}
if($makePreview){
$previewType = 'jpg';
if(in_array($documentType, array('image/png','image/gif','image/bmp','image/tiff'))){
$previewType = 'jpeg';
if(in_array($documentType, array('png','gif','tiff','psd'))){
// Has transparency
$previewType = 'png';
}
$document->preview = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentTypeData['extension'].'.x'.DOCUMENT_PREVIEW_SIZE.'.'.$previewType;
$document->preview = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType.'.x'.DOCUMENT_PREVIEW_SIZE.'.'.$previewType;
if(!$disk->exists($document->preview)){
// We haven't created a preview yet
$imgManager = new ImageManager($imgManagerConfig);
@ -170,7 +178,7 @@ class DocumentRepository extends BaseRepository
$doc_array = $document->toArray();
if(!empty($base64)){
$mime = !empty($previewType)?Document::$extensions[$previewType]:$documentType;
$mime = Document::$types[!empty($previewType)?$previewType:$documentType]['mime'];
$doc_array['base64'] = 'data:'.$mime.';base64,'.$base64;
}

View File

@ -159,6 +159,12 @@ class AppServiceProvider extends ServiceProvider {
return $str . '</ol>';
});
Form::macro('human_filesize', function($bytes, $decimals = 1) {
$size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB');
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . @$size[$factor];
});
Validator::extend('positive', function($attribute, $value, $parameters) {
return Utils::parseFloat($value) >= 0;
});

View File

@ -31404,10 +31404,10 @@ NINJA.invoiceDocuments = function(invoice) {
for (var i = 0; i < invoice.documents.length; i++) {
var document = invoice.documents[i];
var path = document.base64;
if(!path && (document.preview_url || document.type == 'image/png' || document.type == 'image/jpeg')){
path = 'docs/'+document.public_id+'/'+document.name;
}
if(!path)path = 'docs/'+document.public_id+'/'+document.name;
if(path && (window.pdfMake.vfs[path] || document.base64)){
// Only embed if we actually have an image for it
if(j%3==0){
stackItem = {columns:[]};
stack.push(stackItem);

View File

@ -412,10 +412,10 @@ NINJA.invoiceDocuments = function(invoice) {
for (var i = 0; i < invoice.documents.length; i++) {
var document = invoice.documents[i];
var path = document.base64;
if(!path && (document.preview_url || document.type == 'image/png' || document.type == 'image/jpeg')){
path = 'docs/'+document.public_id+'/'+document.name;
}
if(!path)path = 'docs/'+document.public_id+'/'+document.name;
if(path && (window.pdfMake.vfs[path] || document.base64)){
// Only embed if we actually have an image for it
if(j%3==0){
stackItem = {columns:[]};
stack.push(stackItem);

View File

@ -1107,7 +1107,7 @@ $LANG = array(
'invoice_embed_documents' => 'Embed Documents',
'invoice_embed_documents_help' => 'Include attached images in the invoice.',
'document_email_attachment' => 'Attach Documents',
'download_documents' => 'Download Documents',
'download_documents' => 'Download Documents (:size)',
);
return $LANG;

View File

@ -930,7 +930,7 @@
params:{
_token:"{{ Session::getToken() }}"
},
acceptedFiles:{!! json_encode(implode(',',array_keys(\App\Models\Document::$types))) !!},
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
addRemoveLinks:true,
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
dictDefaultMessage:{!! json_encode(trans('texts.document_upload_message')) !!}
@ -955,9 +955,12 @@
dropzone.emit('addedfile', mockFile);
dropzone.emit('complete', mockFile);
if(document.type().match(/image.*/)){
if(document.preview_url()){
dropzone.emit('thumbnail', mockFile, document.preview_url()||document.url());
}
else if(document.type()=='jpeg' || document.type()=='png' || document.type()=='svg'){
dropzone.emit('thumbnail', mockFile, document.url());
}
dropzone.files.push(mockFile);
}
@endif
@ -1005,7 +1008,9 @@
}
function createInvoiceModel() {
var invoice = ko.toJS(window.model).invoice;
var model = ko.toJS(window.model);
if(!model)return;
var invoice = model.invoice;
invoice.is_pro = {{ Auth::user()->isPro() ? 'true' : 'false' }};
invoice.is_quote = {{ $entityType == ENTITY_QUOTE ? 'true' : 'false' }};
invoice.contact = _.findWhere(invoice.client.contacts, {send_invoice: true});
@ -1335,13 +1340,19 @@
file.public_id = response.document.public_id
model.invoice().documents()[file.index].update(response.document);
refreshPDF(true);
if(response.document.preview_url){
dropzone.emit('thumbnail', file, response.document.preview_url);
}
}
@endif
</script>
@if ($account->isPro() && $account->invoice_embed_documents)
@foreach ($invoice->documents as $document)
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@if($document->isPDFEmbeddable())
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@endif
@endforeach
@endif

View File

@ -59,7 +59,9 @@
@if (Auth::user()->account->isPro() && Auth::user()->account->invoice_embed_documents)
@foreach ($invoice->documents as $document)
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@if($document->isPDFEmbeddable())
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@endif
@endforeach
@endif
@stop

View File

@ -43,7 +43,7 @@
</div>
<div class="pull-left">
@if(!empty($documentsZipURL))
{!! Button::normal(trans('texts.download_documents'))->asLinkTo($documentsZipURL)->large() !!}
{!! Button::normal(trans('texts.download_documents', array('size'=>Form::human_filesize($documentsZipSize))))->asLinkTo($documentsZipURL)->large() !!}
@endif
</div>
@endif
@ -54,7 +54,7 @@
<h3>{{ trans('texts.documents_header') }}</h3>
<ul>
@foreach ($invoice->documents as $document)
<li><a target="_blank" href="{{ $document->getClientUrl($invitation) }}">{{$document->name}}</a></li>
<li><a target="_blank" href="{{ $document->getClientUrl($invitation) }}">{{$document->name}} ({{Form::human_filesize($document->size)}})</a></li>
@endforeach
</ul>
</div>
@ -62,7 +62,9 @@
@if ($account->isPro() && $account->invoice_embed_documents)
@foreach ($invoice->documents as $document)
<script src="{{ $document->getClientVFSJSUrl() }}" type="text/javascript" {{ Input::has('phantomjs')?'':'async' }}></script>
@if($document->isPDFEmbeddable())
<script src="{{ $document->getClientVFSJSUrl() }}" type="text/javascript" {{ Input::has('phantomjs')?'':'async' }}></script>
@endif
@endforeach
@endif
<script type="text/javascript">