• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nepada / file-upload-control / 29635366186

18 Jul 2026 07:12AM UTC coverage: 81.481% (-0.3%) from 81.736%
29635366186

push

github

xificurk
Fix build against latest Latte - {spaceless} is now truely spaceless

726 of 891 relevant lines covered (81.48%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.28
/src/FileUploadControl/FileUploadControl.php
1
<?php
2
declare(strict_types = 1);
3

4
namespace Nepada\FileUploadControl;
5

6
use Nepada\FileUploadControl\Responses\ListResponse;
7
use Nepada\FileUploadControl\Responses\Response;
8
use Nepada\FileUploadControl\Responses\UploadErrorResponse;
9
use Nepada\FileUploadControl\Responses\UploadSuccessResponse;
10
use Nepada\FileUploadControl\Storage\ContentRange;
11
use Nepada\FileUploadControl\Storage\FailedUpload;
12
use Nepada\FileUploadControl\Storage\FileUploadChunk;
13
use Nepada\FileUploadControl\Storage\FileUploadId;
14
use Nepada\FileUploadControl\Storage\FileUploadItem;
15
use Nepada\FileUploadControl\Storage\FileUploadNotFoundException;
16
use Nepada\FileUploadControl\Storage\Storage;
17
use Nepada\FileUploadControl\Storage\StorageDoesNotExistException;
18
use Nepada\FileUploadControl\Storage\StorageManager;
19
use Nepada\FileUploadControl\Storage\UnableToSaveFileUploadException;
20
use Nepada\FileUploadControl\Storage\UploadNamespace;
21
use Nepada\FileUploadControl\Thumbnail\NullThumbnailProvider;
22
use Nepada\FileUploadControl\Thumbnail\ThumbnailProvider;
23
use Nepada\FileUploadControl\Validation\ClientSide;
24
use Nepada\FileUploadControl\Validation\FakeUploadControl;
25
use Nepada\FileUploadControl\Validation\UploadValidation;
26
use Nette;
27
use Nette\Bridges\ApplicationLatte\Template;
28
use Nette\Forms\Form;
29
use Nette\Http\FileUpload;
30
use Nette\Utils\Arrays;
31
use Nette\Utils\Html;
32
use Stringable;
33
use function implode;
34

35
class FileUploadControl extends BaseControl
36
{
37

38
    use UploadValidation {
39
        UploadValidation::addRule as private _addRule;
40
    }
41

42
    final public const TEMPLATE_FILE_BOOTSTRAP4 = __DIR__ . '/templates/bootstrap4.latte';
43
    final public const TEMPLATE_FILE_BOOTSTRAP5 = __DIR__ . '/templates/bootstrap5.latte';
44
    final public const DEFAULT_TEMPLATE_FILE = self::TEMPLATE_FILE_BOOTSTRAP4;
45

46
    private string $templateFile = self::DEFAULT_TEMPLATE_FILE;
47

48
    private StorageManager $storageManager;
49

50
    private ThumbnailProvider $thumbnailProvider;
51

52
    private bool $httpDataLoaded = false;
53

54
    public function __construct(StorageManager $storageManager, string|\Stringable|null $caption = null)
1✔
55
    {
56
        parent::__construct($caption);
1✔
57
        $this->storageManager = $storageManager;
1✔
58
        $this->thumbnailProvider = new NullThumbnailProvider();
1✔
59
        $this->control = Html::el();
1✔
60
        $this->setOption('type', 'file-upload');
1✔
61
        $this->addComponent(new Nette\Forms\Controls\UploadControl($caption, true), 'upload');
1✔
62
        $this->addComponent(new Nette\Forms\Controls\HiddenField(), 'namespace');
1✔
63
        $this->initializeValidation($this);
1✔
64
        /** @var string $errorMessage */
65
        $errorMessage = Nette\Forms\Validator::$messages[Nette\Forms\Controls\UploadControl::Valid];
1✔
66
        $this->addCondition(fn () => $this->getPresenterIfExists()?->isSignalReceiver($this, 'upload') !== true) // disable during file upload via signal
1✔
67
            ->addRule($this->validateUploadSuccess(...), $errorMessage);
1✔
68
        $this->addRule(ClientSide::NO_UPLOAD_IN_PROGRESS, 'File upload is still in progress - wait until it is finished, or abort it.');
1✔
69
    }
1✔
70

71
    private function validateUploadSuccess(FakeUploadControl $control): bool
1✔
72
    {
73
        return Arrays::every($control->getValue(), fn (FileUpload $upload): bool => $upload->isOk());
1✔
74
    }
75

76
    public function setThumbnailProvider(ThumbnailProvider $thumbnailProvider): void
1✔
77
    {
78
        $this->thumbnailProvider = $thumbnailProvider;
1✔
79
    }
1✔
80

81
    public function setTemplateFile(string $templateFile): void
1✔
82
    {
83
        $this->templateFile = $templateFile;
1✔
84
    }
1✔
85

86
    /**
87
     * @throws Nette\Application\BadRequestException
88
     */
89
    public function loadHttpData(): void
90
    {
91
        $this->getNamespaceControl()->loadHttpData();
1✔
92
        try {
93
            $storage = $this->getStorage();
1✔
94
        } catch (StorageDoesNotExistException $exception) {
×
95
            // refresh namespace
96
            $this->setUploadNamespace($this->storageManager->createNewNamespace());
×
97
            try {
98
                $storage = $this->getStorage();
×
99
            } catch (StorageDoesNotExistException $exception) {
×
100
                throw new \LogicException($exception->getMessage(), 0, $exception);
×
101
            }
102
        }
103

104
        if ($this->httpDataLoaded || $this->isDisabled()) {
1✔
105
            return;
×
106
        }
107

108
        $this->httpDataLoaded = true;
1✔
109

110
        $fileUploadChunks = $this->getFileUploadChunks();
1✔
111
        if (count($fileUploadChunks) === 0) {
1✔
112
            return;
1✔
113
        }
114

115
        $uploadFailed = false;
1✔
116
        foreach ($fileUploadChunks as $fileUploadChunk) {
1✔
117
            if ($fileUploadChunk instanceof FailedUpload) {
1✔
118
                $uploadFailed = true;
1✔
119
                continue;
1✔
120
            }
121
            try {
122
                $storage->save($fileUploadChunk);
1✔
123
            } catch (UnableToSaveFileUploadException $exception) {
×
124
                $uploadFailed = true;
×
125
            }
126
        }
127
        if ($uploadFailed) {
1✔
128
            /** @var string $errorMessage */
129
            $errorMessage = Nette\Forms\Validator::$messages[Nette\Forms\Controls\UploadControl::Valid];
1✔
130
            $this->addError($errorMessage);
1✔
131
        }
132
    }
1✔
133

134
    /**
135
     * @return list<FileUpload>
136
     */
137
    public function getValue(): array
138
    {
139
        if ($this->isDisabled()) {
1✔
140
            return [];
1✔
141
        }
142

143
        try {
144
            return array_map(fn (FileUploadItem $fileUploadItem): FileUpload => $fileUploadItem->fileUpload, $this->getStorage()->list());
1✔
145
        } catch (StorageDoesNotExistException $exception) {
×
146
            throw new \LogicException($exception->getMessage(), 0, $exception);
×
147
        }
148
    }
149

150
    /**
151
     * @return $this
152
     * @internal
153
     */
154
    public function setValue(mixed $value): static
1✔
155
    {
156
        return $this;
1✔
157
    }
158

159
    public function isDisabled(): bool
160
    {
161
        return $this->getUploadControl()->isDisabled();
1✔
162
    }
163

164
    /**
165
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
166
     * @param bool $value
167
     * @return $this
168
     * @throws Nette\Application\BadRequestException
169
     */
170
    public function setDisabled($value = true): static
1✔
171
    {
172
        $this->getUploadControl()->setDisabled($value);
1✔
173
        if (! $value) {
1✔
174
            return $this;
×
175
        }
176

177
        $form = $this->getForm(false);
1✔
178
        if ($form !== null && $form->isAnchored() && (bool) $form->isSubmitted()) {
1✔
179
            $this->loadHttpData();
×
180
        }
181

182
        return $this;
1✔
183
    }
184

185
    public function getControlPart(?string $key = null): ?Html
1✔
186
    {
187
        if ($key === 'namespace') {
1✔
188
            return $this->getNamespaceControl()->getControl();
1✔
189
        }
190

191
        if ($key === 'upload') {
1✔
192
            $control = $this->getUploadControl()->getControl();
1✔
193
            assert($control instanceof Html);
1✔
194
            $control->{'data-nette-rules'} = Nette\Forms\Helpers::exportRules($this->getRules());
1✔
195
            return $control;
1✔
196
        }
197

198
        return $this->getControl();
×
199
    }
200

201
    public function getControl(): Html
202
    {
203
        $this->setOption('rendered', true);
1✔
204
        $control = clone $this->control;
1✔
205

206
        $template = $this->getTemplate();
1✔
207
        assert($template instanceof Template);
1✔
208

209
        try {
210
            $storage = $this->getStorage();
1✔
211
        } catch (StorageDoesNotExistException $exception) {
×
212
            throw new \LogicException($exception->getMessage(), 0, $exception);
×
213
        }
214
        $fileUploadItems = $storage->list();
1✔
215

216
        $uniqueFilenames = array_map(fn (FileUploadItem $fileUploadItem): string => $fileUploadItem->fileUpload->getUntrustedName(), $fileUploadItems);
1✔
217
        $completedFiles = [];
1✔
218
        $partiallyUploadedFiles = [];
1✔
219
        foreach ($fileUploadItems as $fileUploadItem) {
1✔
220
            if ($fileUploadItem->fileUpload->isOk()) {
1✔
221
                $completedFiles[] = $this->createUploadSuccessResponse($fileUploadItem);
1✔
222
            } else {
223
                $partiallyUploadedFiles[] = $this->createUploadSuccessResponse($fileUploadItem);
1✔
224
            }
225
        }
226

227
        $template->uniqueFilenames = $uniqueFilenames;
1✔
228
        $template->completedFiles = $completedFiles;
1✔
229
        $template->partiallyUploadedFiles = $partiallyUploadedFiles;
1✔
230
        $template->allFiles = array_merge($completedFiles, $partiallyUploadedFiles);
1✔
231
        $template->uploadUrl = $this->link('upload!', ['namespace' => $this->getUploadNamespace()->toString()]);
1✔
232

233
        $controlHtml = Nette\Utils\Helpers::capture(function () use ($template): void {
1✔
234
            $template->render($this->templateFile);
1✔
235
        });
1✔
236
        return $control->addHtml($controlHtml);
1✔
237
    }
238

239
    public function getLabelPrototype(): Html
240
    {
241
        return $this->getUploadControl()->getLabelPrototype();
1✔
242
    }
243

244
    public function getLabel(string|Stringable|null $caption = null): Html
1✔
245
    {
246
        $label = $this->getUploadControl()->getLabel($caption);
1✔
247
        assert($label instanceof Html);
1✔
248
        return $label;
1✔
249
    }
250

251
    /**
252
     * @return $this
253
     */
254
    public function addRule(callable|string $validator, string|Stringable|null $errorMessage = null, mixed $arg = null): static
1✔
255
    {
256
        if ($validator === Form::Image) {
1✔
257
            /** @var list<string> $mimeTypes */
258
            $mimeTypes = Nette\Forms\Helpers::getSupportedImages();
1✔
259
            $this->getUploadControl()->setHtmlAttribute('accept', implode(',', $mimeTypes));
1✔
260
        } elseif ($validator === Form::MimeType) {
1✔
261
            /** @var list<string> $mimeTypes */
262
            $mimeTypes = is_array($arg) ? $arg : ($arg === null ? [] : [$arg]);
1✔
263
            $this->getUploadControl()->setHtmlAttribute('accept', implode(',', $mimeTypes));
1✔
264
        }
265

266
        return $this->_addRule($validator, $errorMessage, $arg);
1✔
267
    }
268

269
    /**
270
     * @throws Nette\Application\BadRequestException
271
     */
272
    public function handleUpload(string $namespace): void
1✔
273
    {
274
        $uploadNamespace = $this->parseUploadNamespace($namespace);
1✔
275
        $this->setUploadNamespace($uploadNamespace);
1✔
276

277
        $fileUploadChunks = $this->getFileUploadChunks();
1✔
278
        $responses = [];
1✔
279
        foreach ($fileUploadChunks as $fileUploadChunk) {
1✔
280
            if ($this->isDisabled()) {
1✔
281
                $responses[] = $this->createUploadErrorResponse($fileUploadChunk, $this->translate('Upload disabled'));
1✔
282
                continue;
1✔
283
            }
284

285
            if ($fileUploadChunk instanceof FailedUpload) {
1✔
286
                $responses[] = $this->createUploadErrorResponse($fileUploadChunk, $this->translate(Nette\Forms\Validator::$messages[Nette\Forms\Controls\UploadControl::Valid]));
1✔
287
                continue;
1✔
288
            }
289

290
            if ($fileUploadChunk->contentRange->containsFirstByte()) {
1✔
291
                $fakeFileUpload = new FileUpload([
1✔
292
                    'name' => $fileUploadChunk->fileUpload->getUntrustedName(),
1✔
293
                    'size' => $fileUploadChunk->contentRange->getSize(),
1✔
294
                    'tmp_name' => $fileUploadChunk->fileUpload->getTemporaryFile(),
1✔
295
                    'error' => UPLOAD_ERR_OK,
1✔
296
                ]);
297
                $this->fakeUploadControl->setNewFileUpload($fakeFileUpload);
1✔
298
                $this->validate();
1✔
299
                $error = $this->getError();
1✔
300
                if ($error !== null) {
1✔
301
                    $responses[] = $this->createUploadErrorResponse($fileUploadChunk, $error);
1✔
302
                    continue;
1✔
303
                }
304
            }
305

306
            try {
307
                $fileUploadItem = $this->getStorage()->save($fileUploadChunk);
1✔
308
                $responses[] = $this->createUploadSuccessResponse($fileUploadItem);
1✔
309
            } catch (StorageDoesNotExistException | UnableToSaveFileUploadException $exception) {
×
310
                $responses[] = $this->createUploadErrorResponse($fileUploadChunk, $this->translate(Nette\Forms\Validator::$messages[Nette\Forms\Controls\UploadControl::Valid]));
×
311
            }
312
        }
313

314
        $this->sendJson(new ListResponse(...$responses));
1✔
315
    }
316

317
    /**
318
     * @throws Nette\Application\BadRequestException
319
     */
320
    public function handleDelete(string $namespace, string $id): void
321
    {
322
        $fileUploadId = $this->parseFileUploadId($id);
×
323
        $uploadNamespace = $this->parseUploadNamespace($namespace);
×
324
        $this->setUploadNamespace($uploadNamespace);
×
325

326
        try {
327
            $this->getStorage()->delete($fileUploadId);
×
328
        } catch (StorageDoesNotExistException $exception) {
×
329
            // noop
330
        }
331
        $this->sendJson('');
×
332
    }
333

334
    /**
335
     * @throws Nette\Application\BadRequestException
336
     */
337
    public function handleDownload(string $namespace, string $id): void
1✔
338
    {
339
        $fileUploadId = $this->parseFileUploadId($id);
1✔
340
        $uploadNamespace = $this->parseUploadNamespace($namespace);
1✔
341
        $this->setUploadNamespace($uploadNamespace);
1✔
342

343
        try {
344
            $fileUploadItem = $this->getStorage()->load($fileUploadId);
1✔
345
        } catch (StorageDoesNotExistException | FileUploadNotFoundException $exception) {
×
346
            throw new Nette\Application\BadRequestException('File upload not found.', 0, $exception);
×
347
        }
348

349
        $fileUpload = $fileUploadItem->fileUpload;
1✔
350
        $response = new Nette\Application\Responses\FileResponse(
1✔
351
            $fileUpload->getTemporaryFile(),
1✔
352
            $fileUpload->getUntrustedName(),
1✔
353
        );
354
        $this->getPresenter()->sendResponse($response);
1✔
355
    }
356

357
    /**
358
     * @throws Nette\Application\BadRequestException
359
     */
360
    public function handleThumbnail(string $namespace, string $id): void
361
    {
362
        $fileUploadId = $this->parseFileUploadId($id);
×
363
        $uploadNamespace = $this->parseUploadNamespace($namespace);
×
364
        $this->setUploadNamespace($uploadNamespace);
×
365

366
        try {
367
            $fileUpload = $this->getStorage()->load($fileUploadId)->fileUpload;
×
368
        } catch (StorageDoesNotExistException | FileUploadNotFoundException $exception) {
×
369
            throw new Nette\Application\BadRequestException('Source file for thumbnail not found.', 0, $exception);
×
370
        }
371

372
        if (! $this->thumbnailProvider->isSupported($fileUpload)) {
×
373
            throw new Nette\Application\BadRequestException('Thumbnail could not be generated');
×
374
        }
375

376
        $response = $this->thumbnailProvider->createThumbnail($fileUpload);
×
377
        $this->getPresenter()->sendResponse($response);
×
378
    }
379

380
    /**
381
     * @template T of Template
382
     * @param ?class-string<T>  $class
383
     * @return ($class is null ? Template : T)
384
     */
385
    protected function createTemplate(?string $class = null): Template
1✔
386
    {
387
        $template = parent::createTemplate($class);
1✔
388
        assert($template instanceof Template);
1✔
389

390
        $translator = $this->getTranslator();
1✔
391
        if ($translator !== null) {
1✔
392
            $template->setTranslator($translator);
1✔
393
        }
394

395
        $template->getLatte()->addFilter('json', fn (mixed $data): string => Nette\Utils\Json::encode($data));
1✔
396

397
        return $template;
1✔
398
    }
399

400
    protected function getHttpRequest(): Nette\Http\IRequest
401
    {
402
        // With nette/component-model 4.x, monitor callbacks fire top-down (ancestor first). When UI\Form's
403
        // Presenter-attach callback triggers loadHttpData() on this control, a direct $this->getPresenter()
404
        // would still hit a stale negative lookup cache and throw, even though the tree is fully attached.
405
        // Reaching the presenter through the form is safe - the form's own lookup is fresh inside its callback.
406
        $form = $this->getForm(false);
1✔
407
        $presenter = $form instanceof Nette\Application\UI\Form ? $form->getPresenter() : null;
1✔
408
        return ($presenter ?? $this->getPresenter())->getHttpRequest();
1✔
409
    }
410

411
    /**
412
     * Sends back JSON response.
413
     * Sets the right content type based on the support on the other end.
414
     * https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#wiki-content-type-negotiation
415
     */
416
    protected function sendJson(mixed $data): void
1✔
417
    {
418
        $contentType = str_contains((string) $this->getHttpRequest()->getHeader('accept'), 'application/json') ? 'application/json' : 'text/plain';
1✔
419
        $response = new Nette\Application\Responses\JsonResponse($data, $contentType);
1✔
420
        $this->getPresenter()->sendResponse($response);
1✔
421
    }
422

423
    protected function getUploadControl(): Nette\Forms\Controls\UploadControl
424
    {
425
        return $this->getComponent('upload');
1✔
426
    }
427

428
    protected function getNamespaceControl(): Nette\Forms\Controls\HiddenField
429
    {
430
        return $this->getComponent('namespace');
1✔
431
    }
432

433
    protected function getThumbnailProvider(): ThumbnailProvider
434
    {
435
        return $this->thumbnailProvider;
×
436
    }
437

438
    protected function getUploadNamespace(): UploadNamespace
439
    {
440
        /** @var string|null $nameSpaceValue */
441
        $nameSpaceValue = $this->getNamespaceControl()->getValue();
1✔
442
        if ($nameSpaceValue !== null && UploadNamespace::isValid($nameSpaceValue)) {
1✔
443
            return UploadNamespace::fromString($nameSpaceValue);
1✔
444
        }
445

446
        $namespace = $this->storageManager->createNewNamespace();
×
447
        $this->setUploadNamespace($namespace);
×
448
        return $namespace;
×
449
    }
450

451
    protected function setUploadNamespace(UploadNamespace $namespace): void
1✔
452
    {
453
        $this->getNamespaceControl()->setValue($namespace->toString());
1✔
454
    }
1✔
455

456
    /**
457
     * @throws StorageDoesNotExistException
458
     */
459
    protected function getStorage(): Storage
460
    {
461
        return $this->storageManager->getStorage($this->getUploadNamespace());
1✔
462
    }
463

464
    protected function createUploadSuccessResponse(FileUploadItem $fileUploadItem): Response
1✔
465
    {
466
        $fileUpload = $fileUploadItem->fileUpload;
1✔
467
        $idValue = $fileUploadItem->id->toString();
1✔
468
        $namespaceValue = $this->getUploadNamespace()->toString();
1✔
469
        return new UploadSuccessResponse(
1✔
470
            $fileUpload->getUntrustedName(),
1✔
471
            $fileUpload->getSize(),
1✔
472
            $fileUpload->getContentType(),
1✔
473
            $this->link('download!', ['namespace' => $namespaceValue, 'id' => $idValue]),
1✔
474
            $this->link('delete!', ['namespace' => $namespaceValue, 'id' => $idValue]),
1✔
475
            $this->thumbnailProvider->isSupported($fileUpload) ? $this->link('thumbnail!', ['namespace' => $namespaceValue, 'id' => $idValue]) : null,
1✔
476
        );
477
    }
478

479
    protected function createUploadErrorResponse(FileUploadChunk|FailedUpload $upload, string $error): Response
1✔
480
    {
481
        return new UploadErrorResponse(
1✔
482
            $upload->fileUpload->getUntrustedName(),
1✔
483
            $upload->contentRange?->getSize() ?? 0,
1✔
484
            $error,
485
        );
486
    }
487

488
    /**
489
     * @throws Nette\Application\BadRequestException
490
     */
491
    protected function parseUploadNamespace(string $value): UploadNamespace
1✔
492
    {
493
        if (! UploadNamespace::isValid($value)) {
1✔
494
            throw new Nette\Application\BadRequestException('Invalid namespace value', 400);
×
495
        }
496
        return UploadNamespace::fromString($value);
1✔
497
    }
498

499
    /**
500
     * @throws Nette\Application\BadRequestException
501
     */
502
    protected function parseFileUploadId(string $value): FileUploadId
1✔
503
    {
504
        if (! FileUploadId::isValid($value)) {
1✔
505
            throw new Nette\Application\BadRequestException('Invalid file upload id value', 400);
×
506
        }
507
        return FileUploadId::fromString($value);
1✔
508
    }
509

510
    /**
511
     * @return list<FileUploadChunk|FailedUpload>
512
     * @throws Nette\Application\BadRequestException
513
     */
514
    protected function getFileUploadChunks(): array
515
    {
516
        $httpRequest = $this->getHttpRequest();
1✔
517
        /** @var array<int, FileUpload> $files */
518
        $files = Nette\Forms\Helpers::extractHttpData($httpRequest->getFiles(), $this->getUploadControl()->getHtmlName() . '[]', Form::DataFile);
1✔
519
        if (count($files) === 0) {
1✔
520
            return [];
1✔
521
        }
522

523
        $contentRangeHeaderValue = $httpRequest->getHeader('content-range');
1✔
524
        if ($contentRangeHeaderValue !== null) {
1✔
525
            if (count($files) > 1) {
1✔
526
                throw new Nette\Application\BadRequestException('Chunk upload does not support multi-file upload', 400);
×
527
            }
528
            $file = reset($files);
1✔
529

530
            try {
531
                $contentRange = ContentRange::fromHttpHeaderValue($contentRangeHeaderValue);
1✔
532
                $fileUpload = $file->isOk() ? FileUploadChunk::partialUpload($file, $contentRange) : FailedUpload::of($file, $contentRange);
1✔
533
                return [$fileUpload];
1✔
534
            } catch (\Throwable $exception) {
×
535
                throw new Nette\Application\BadRequestException('Invalid content-range header value', 400, $exception);
×
536
            }
537
        }
538

539
        /** @var list<FileUploadChunk|FailedUpload> $fileUploads */
540
        $fileUploads = [];
1✔
541
        foreach ($files as $file) {
1✔
542
            $fileUploads[] = $file->isOk() ? FileUploadChunk::completeUpload($file) : FailedUpload::of($file);
1✔
543
        }
544

545
        return $fileUploads;
1✔
546
    }
547

548
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc