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

LibreSign / libresign / 21006909149

14 Jan 2026 07:21PM UTC coverage: 43.837%. First build
21006909149

Pull #6436

github

web-flow
Merge 1b659c916 into 9bd4c65c5
Pull Request #6436: feat: async parallel signing

294 of 825 new or added lines in 35 files covered. (35.64%)

6921 of 15788 relevant lines covered (43.84%)

4.87 hits per line

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

0.0
/lib/Controller/FileProgressController.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Controller;
10

11
use OCA\Libresign\Db\File as FileEntity;
12
use OCA\Libresign\Db\FileMapper;
13
use OCA\Libresign\Db\SignRequestMapper;
14
use OCA\Libresign\Enum\FileStatus;
15
use OCA\Libresign\Service\WorkerHealthService;
16
use OCP\AppFramework\Http;
17
use OCP\AppFramework\Http\Attribute\ApiRoute;
18
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
19
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
20
use OCP\AppFramework\Http\DataResponse;
21
use OCP\AppFramework\OCSController;
22
use OCP\ICache;
23
use OCP\ICacheFactory;
24
use OCP\IRequest;
25

26
class FileProgressController extends OCSController {
27
        private ICache $cache;
28

29
        public function __construct(
30
                string $appName,
31
                IRequest $request,
32
                private FileMapper $fileMapper,
33
                private SignRequestMapper $signRequestMapper,
34
                private WorkerHealthService $workerHealthService,
35
                ICacheFactory $cacheFactory,
36
        ) {
NEW
37
                parent::__construct($appName, $request);
×
NEW
38
                $this->cache = $cacheFactory->createDistributed('libresign_progress');
×
39
        }
40

41
        /**
42
         * Wait for file/envelope status changes (long polling)
43
         *
44
         * Keeps connection open for up to 30 seconds waiting for status change.
45
         *
46
         * @param int $fileId LibreSign file ID
47
         * @param int $currentStatus Current status known by client
48
         * @param int $timeout Seconds to wait (default 30, max 30)
49
         * @return DataResponse<Http::STATUS_OK, array{status: int, statusText: string, name: string, progress: array<string, mixed>}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{message: string}, array{}>
50
         *
51
         * 200: Status and progress returned
52
         * 404: File not found
53
         */
54
        #[NoAdminRequired]
55
        #[NoCSRFRequired]
56
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/{fileId}/wait-status', requirements: ['apiVersion' => '(v1)'])]
57
        public function waitForStatusChange(
58
                int $fileId,
59
                int $currentStatus,
60
                int $timeout = 30,
61
        ): DataResponse {
NEW
62
                $timeout = min(30, $timeout);
×
NEW
63
                $elapsedTime = 0;
×
64

65
                try {
NEW
66
                        $file = $this->fileMapper->getById($fileId);
×
NEW
67
                        $statusChanged = $file->getStatus() !== $currentStatus;
×
68

NEW
69
                        while (!$statusChanged && $elapsedTime < $timeout) {
×
NEW
70
                                sleep(1);
×
NEW
71
                                $elapsedTime++;
×
72

NEW
73
                                $file = $this->fileMapper->getById($fileId);
×
NEW
74
                                $statusChanged = $file->getStatus() !== $currentStatus;
×
75
                        }
76

NEW
77
                        return new DataResponse([
×
NEW
78
                                'status' => $file->getStatus(),
×
NEW
79
                                'statusText' => $this->fileMapper->getTextOfStatus($file->getStatus()),
×
NEW
80
                                'name' => $file->getName(),
×
NEW
81
                                'progress' => $this->getSigningProgress($file),
×
NEW
82
                        ], Http::STATUS_OK);
×
83

NEW
84
                } catch (\Exception $e) {
×
NEW
85
                        return new DataResponse([
×
NEW
86
                                'message' => $e->getMessage(),
×
NEW
87
                        ], Http::STATUS_NOT_FOUND);
×
88
                }
89
        }
90

91
        /**
92
         * Check file progress by UUID with long-polling (similar to Talk)
93
         *
94
         * Waits up to 30 seconds for status change using cache for efficiency.
95
         *
96
         * @param string $uuid File UUID
97
         * @param int $timeout Maximum seconds to wait (default 30)
98
         * @return DataResponse<Http::STATUS_OK, array{status: string, statusCode: int, statusText: string, fileId: int, progress: array<string, mixed>}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{message: string, status: string}, array{}>
99
         *
100
         * 200: Status and progress returned
101
         * 404: File not found
102
         */
103
        #[NoAdminRequired]
104
        #[NoCSRFRequired]
105
        #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/file/progress/{uuid}', requirements: ['apiVersion' => '(v1)'])]
106
        public function checkProgressByUuid(string $uuid, int $timeout = 30): DataResponse {
107
                try {
NEW
108
                        $file = $this->fileMapper->getByUuid($uuid);
×
NEW
109
                        $currentStatus = $file->getStatus();
×
110

NEW
111
                        if ($currentStatus === FileStatus::SIGNING_IN_PROGRESS->value) {
×
NEW
112
                                $this->workerHealthService->ensureWorkerRunning();
×
NEW
113
                                $currentStatus = $this->pollForStatusChange($uuid, $currentStatus, $timeout);
×
114
                        }
115

NEW
116
                        return $this->buildStatusResponse($file, $currentStatus);
×
117

NEW
118
                } catch (\Exception $e) {
×
NEW
119
                        return new DataResponse([
×
NEW
120
                                'message' => $e->getMessage(),
×
NEW
121
                                'status' => 'ERROR',
×
NEW
122
                        ], Http::STATUS_NOT_FOUND);
×
123
                }
124
        }
125

126
        private function getSigningProgress(FileEntity $file): array {
NEW
127
                if ($file->getNodeType() === 'envelope') {
×
NEW
128
                        return $this->getEnvelopeProgress($file);
×
129
                }
NEW
130
                if (!$file->getParentFileId()) {
×
NEW
131
                        return $this->getSingleFileProgress($file);
×
132
                }
NEW
133
                return $this->getFileProgress($file);
×
134
        }
135

136
        private function pollForStatusChange(string $uuid, int $initialStatus, int $timeout): int {
NEW
137
                $elapsedTime = 0;
×
NEW
138
                $cacheKey = 'status_' . $uuid;
×
NEW
139
                $cachedStatus = $this->cache->get($cacheKey);
×
NEW
140
                $currentStatus = $initialStatus;
×
141

NEW
142
                while ($elapsedTime < $timeout) {
×
NEW
143
                        $newCachedStatus = $this->cache->get($cacheKey);
×
144

NEW
145
                        if ($newCachedStatus !== $cachedStatus && $newCachedStatus !== false) {
×
NEW
146
                                return (int)$newCachedStatus;
×
147
                        }
148

NEW
149
                        sleep(1);
×
NEW
150
                        $elapsedTime++;
×
151
                }
152

NEW
153
                return $currentStatus;
×
154
        }
155

156
        private function buildStatusResponse(FileEntity $file, int $status): DataResponse {
NEW
157
                $statusEnum = FileStatus::tryFrom($status);
×
158

NEW
159
                return new DataResponse([
×
NEW
160
                        'status' => $statusEnum?->name ?? 'UNKNOWN',
×
NEW
161
                        'statusCode' => $status,
×
NEW
162
                        'statusText' => $this->fileMapper->getTextOfStatus($status),
×
NEW
163
                        'fileId' => $file->getId(),
×
NEW
164
                        'progress' => $this->getSigningProgress($file),
×
NEW
165
                ], Http::STATUS_OK);
×
166
        }
167

168
        private function getSingleFileProgress(FileEntity $file): array {
NEW
169
                return [
×
NEW
170
                        'total' => 1,
×
NEW
171
                        'signed' => $file->getStatus() === FileStatus::SIGNED->value ? 1 : 0,
×
NEW
172
                        'inProgress' => $file->getStatus() === FileStatus::SIGNING_IN_PROGRESS->value ? 1 : 0,
×
NEW
173
                        'pending' => $file->getStatus() === FileStatus::SIGNING_IN_PROGRESS->value ? 0 : ($file->getStatus() === FileStatus::SIGNED->value ? 0 : 1),
×
NEW
174
                        'files' => [
×
NEW
175
                                [
×
NEW
176
                                        'id' => $file->getId(),
×
NEW
177
                                        'name' => $file->getName(),
×
NEW
178
                                        'status' => $file->getStatus(),
×
NEW
179
                                        'statusText' => $this->fileMapper->getTextOfStatus($file->getStatus()),
×
NEW
180
                                ]
×
NEW
181
                        ],
×
NEW
182
                ];
×
183
        }
184

185
        private function getEnvelopeProgress(FileEntity $envelope): array {
NEW
186
                $children = $this->fileMapper->getChildrenFiles($envelope->getId());
×
NEW
187
                if (empty($children)) {
×
NEW
188
                        $children = [$envelope];
×
189
                }
190

NEW
191
                $totals = $this->countStatusTotals($children);
×
NEW
192
                $files = array_map(fn ($child) => $this->mapFileProgress($child), $children);
×
193

NEW
194
                return $totals + ['files' => $files];
×
195
        }
196

197
        private function countStatusTotals(array $children): array {
NEW
198
                $totals = ['total' => count($children), 'signed' => 0, 'inProgress' => 0, 'pending' => 0];
×
199

NEW
200
                foreach ($children as $child) {
×
NEW
201
                        match ($child->getStatus()) {
×
NEW
202
                                FileStatus::SIGNED->value => $totals['signed']++,
×
NEW
203
                                FileStatus::SIGNING_IN_PROGRESS->value => $totals['inProgress']++,
×
NEW
204
                                default => $totals['pending']++,
×
NEW
205
                        };
×
206
                }
207

NEW
208
                return $totals;
×
209
        }
210

211
        private function mapFileProgress(FileEntity $file): array {
NEW
212
                return [
×
NEW
213
                        'id' => $file->getId(),
×
NEW
214
                        'name' => $file->getName(),
×
NEW
215
                        'status' => $file->getStatus(),
×
NEW
216
                        'statusText' => $this->fileMapper->getTextOfStatus($file->getStatus()),
×
NEW
217
                ];
×
218
        }
219

220
        private function getFileProgress(FileEntity $file): array {
NEW
221
                $signRequests = $this->signRequestMapper->getByFileId($file->getId());
×
222

NEW
223
                $total = count($signRequests);
×
NEW
224
                $signed = count(array_filter($signRequests, fn ($sr) => $sr->getSigned() !== null));
×
225

NEW
226
                return [
×
NEW
227
                        'total' => $total,
×
NEW
228
                        'signed' => $signed,
×
NEW
229
                        'pending' => $total - $signed,
×
NEW
230
                        'signers' => array_map(function ($sr) {
×
NEW
231
                                return [
×
NEW
232
                                        'id' => $sr->getId(),
×
NEW
233
                                        'displayName' => $sr->getDisplayName(),
×
NEW
234
                                        'signed' => $sr->getSigned() ? $sr->getSigned()->format('c') : null,
×
NEW
235
                                        'status' => $sr->getStatus(),
×
NEW
236
                                ];
×
NEW
237
                        }, $signRequests),
×
NEW
238
                ];
×
239
        }
240
}
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