This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/sbp by this push:
     new 03f4030d Combine multiple uploaded files into a single JS request
03f4030d is described below

commit 03f4030db5fb678c40abf1a1d6b57286934b6051
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Apr 6 20:29:04 2026 +0100

    Combine multiple uploaded files into a single JS request
---
 atr/static/js/src/upload-progress-ui.js | 305 ++++++++++++++++++--------------
 atr/static/js/src/upload-progress.js    | 279 ++++++++---------------------
 2 files changed, 247 insertions(+), 337 deletions(-)

diff --git a/atr/static/js/src/upload-progress-ui.js 
b/atr/static/js/src/upload-progress-ui.js
index cf482100..0a6b4b22 100644
--- a/atr/static/js/src/upload-progress-ui.js
+++ b/atr/static/js/src/upload-progress-ui.js
@@ -18,166 +18,201 @@
  */
 
 window.UploadUI = {
-       formatBytes(b) {
-               if (b < 1024) return `${b} B`;
-               if (b < 1048576) return `${(b / 1024).toFixed(1)} KB`;
-               if (b < 1073741824) return `${(b / 1048576).toFixed(1)} MB`;
-               return `${(b / 1073741824).toFixed(2)} GB`;
-       },
-
-       buildFileProgressCard(fileName, fileSizeText) {
-               const div = document.createElement("div");
-               div.className = "card mb-3";
-
-               const cardBody = document.createElement("div");
-               cardBody.className = "card-body";
-
-               const headerRow = document.createElement("div");
-               headerRow.className =
-                       "d-flex justify-content-between align-items-start mb-2";
+       buildBatchUI(container, files, onCancel) {
+               container.innerHTML = "";
+               container.classList.remove("d-none");
+
+               container.append(this.buildHeader(onCancel));
+               container.append(this.buildProgressBar());
+
+               const list = document.createElement("div");
+               list.id = "upload-file-list";
+               files.forEach((file, i) => {
+                       const row = document.createElement("div");
+                       row.className =
+                               "d-flex justify-content-between 
align-items-center py-1 border-bottom";
+                       row.dataset.fileIndex = String(i);
+                       const nameSpan = document.createElement("span");
+                       nameSpan.textContent = `${file.name} 
(${this.formatBytes(file.size)})`;
+                       const statusSpan = document.createElement("small");
+                       statusSpan.className = "upload-file-status text-muted";
+                       statusSpan.textContent = "Pending";
+                       row.append(nameSpan, statusSpan);
+                       list.append(row);
+               });
+               container.append(list);
 
-               const fileInfoDiv = document.createElement("div");
-               const fileNameEl = document.createElement("strong");
-               fileNameEl.className = "file-name";
-               fileNameEl.textContent = fileName;
-               const fileSize = document.createElement("small");
-               fileSize.className = "text-muted ms-2";
-               fileSize.textContent = `(${fileSizeText})`;
-               fileInfoDiv.append(fileNameEl, fileSize);
+               const msgArea = document.createElement("div");
+               msgArea.id = "upload-message-area";
+               container.append(msgArea);
+       },
 
+       buildHeader(onCancel) {
+               const header = document.createElement("div");
+               header.className = "d-flex justify-content-between 
align-items-center mb-3";
+               const statusEl = document.createElement("strong");
+               statusEl.id = "upload-batch-status";
+               statusEl.textContent = "Preparing upload";
                const cancelBtn = document.createElement("button");
                cancelBtn.type = "button";
-               cancelBtn.className = "btn btn-sm btn-outline-secondary 
cancel-btn";
-               cancelBtn.textContent = "Cancel";
-               headerRow.append(fileInfoDiv, cancelBtn);
+               cancelBtn.id = "upload-cancel-btn";
+               cancelBtn.className = "btn btn-sm btn-outline-secondary";
+               cancelBtn.textContent = "Cancel upload";
+               cancelBtn.addEventListener("click", onCancel);
+               header.append(statusEl, cancelBtn);
+               return header;
+       },
 
+       buildProgressBar() {
+               const wrap = document.createElement("div");
+               wrap.className = "mb-3";
                const progress = document.createElement("progress");
+               progress.id = "upload-overall-progress";
                progress.className = "w-100 mb-1";
                progress.value = 0;
                progress.max = 100;
-
-               const statusRow = document.createElement("div");
-               statusRow.className = "d-flex justify-content-between";
-               const uploadStatus = document.createElement("small");
-               uploadStatus.className = "upload-status text-muted";
-               uploadStatus.textContent = "Preparing...";
-               const uploadPercent = document.createElement("small");
-               uploadPercent.className = "upload-percent";
-               uploadPercent.textContent = "0%";
-               statusRow.append(uploadStatus, uploadPercent);
-
-               cardBody.append(headerRow, progress, statusRow);
-               div.append(cardBody);
-               return div;
+               const info = document.createElement("div");
+               info.className = "d-flex justify-content-between";
+               const bytes = document.createElement("small");
+               bytes.id = "upload-progress-bytes";
+               bytes.className = "text-muted";
+               const percent = document.createElement("small");
+               percent.id = "upload-progress-percent";
+               percent.textContent = "0%";
+               info.append(bytes, percent);
+               wrap.append(progress, info);
+               return wrap;
        },
 
-       createFileProgressUI(file, index, activeUploads) {
-               const div = this.buildFileProgressCard(
-                       file.name,
-                       this.formatBytes(file.size),
-               );
-               div.id = `upload-file-${index}`;
-               div.querySelector(".cancel-btn").addEventListener("click", () 
=> {
-                       const xhr = activeUploads.get(index);
-                       if (xhr) xhr.abort();
-               });
-               return div;
+       formatBytes(b) {
+               if (b < 1024) return `${b} B`;
+               if (b < 1048576) return `${(b / 1024).toFixed(1)} KB`;
+               if (b < 1073741824) return `${(b / 1048576).toFixed(1)} MB`;
+               return `${(b / 1073741824).toFixed(2)} GB`;
        },
 
-       markUploadDone(ui, success, msg) {
-               const statusEl = ui.querySelector(".upload-status");
-               statusEl.textContent = msg;
-               statusEl.className = `upload-status text-${success ? "success" 
: "danger"}`;
-               const border = success ? "border-success" : "border-danger";
-               ui.querySelector(".card-body").classList.add(
-                       "border-start",
-                       border,
-                       "border-3",
-               );
-               if (success) {
-                       ui.querySelector("progress").value = 100;
-                       ui.querySelector(".upload-percent").textContent = 
"100%";
+       showAmbiguousError() {
+               const statusEl = document.getElementById("upload-batch-status");
+               if (statusEl) statusEl.textContent = "Upload failed";
+               const cancelBtn = document.getElementById("upload-cancel-btn");
+               if (cancelBtn) cancelBtn.classList.add("d-none");
+               const msgArea = document.getElementById("upload-message-area");
+               if (msgArea) {
+                       msgArea.innerHTML = "";
+                       const alert = document.createElement("div");
+                       alert.className = "alert alert-warning mt-3";
+                       alert.textContent =
+                               "Your files may already have been uploaded. 
Check compose before retrying.";
+                       msgArea.append(alert);
                }
-               ui.querySelector(".cancel-btn").classList.add("d-none");
        },
 
-       resetUploadUI(ui) {
-               ui.querySelector(".card-body").classList.remove(
-                       "border-start",
-                       "border-danger",
-                       "border-3",
-               );
-               const statusEl = ui.querySelector(".upload-status");
-               statusEl.textContent = "Preparing...";
-               statusEl.className = "upload-status text-muted";
-               ui.querySelector("progress").value = 0;
-               ui.querySelector(".upload-percent").textContent = "0%";
-               ui.querySelector(".cancel-btn").classList.remove("d-none");
+       showCancelled() {
+               const statusEl = document.getElementById("upload-batch-status");
+               if (statusEl) statusEl.textContent = "Upload cancelled";
+               const cancelBtn = document.getElementById("upload-cancel-btn");
+               if (cancelBtn) cancelBtn.classList.add("d-none");
        },
 
-       showAllFailedSummary(container) {
-               const summary = document.createElement("div");
-               summary.className = "alert alert-danger mt-3";
-               summary.innerHTML = `<strong>All uploads failed.</strong>
-                       <button type="button" class="btn btn-outline-primary 
ms-3" id="retry-all-btn">Try again</button>`;
-               container.append(summary);
-               document
-                       .getElementById("retry-all-btn")
-                       .addEventListener("click", () => location.reload());
+       showError(message, onRetry) {
+               const statusEl = document.getElementById("upload-batch-status");
+               if (statusEl) statusEl.textContent = "Upload failed";
+               const cancelBtn = document.getElementById("upload-cancel-btn");
+               if (cancelBtn) cancelBtn.classList.add("d-none");
+               const msgArea = document.getElementById("upload-message-area");
+               if (msgArea) {
+                       msgArea.innerHTML = "";
+                       const alert = document.createElement("div");
+                       alert.className = "alert alert-danger mt-3";
+                       const msgEl = document.createElement("span");
+                       msgEl.textContent = message;
+                       alert.append(msgEl);
+                       if (onRetry) {
+                               const retryBtn = 
document.createElement("button");
+                               retryBtn.type = "button";
+                               retryBtn.className = "btn btn-outline-primary 
ms-3";
+                               retryBtn.textContent = "Try again";
+                               retryBtn.addEventListener("click", onRetry);
+                               alert.append(retryBtn);
+                       }
+                       msgArea.append(alert);
+               }
        },
 
-       showPartialSuccessSummary(container, completedCount, totalFiles, 
onFinalise) {
-               const summary = document.createElement("div");
-               summary.className = "alert alert-warning mt-3";
-               summary.id = "upload-summary";
-               const totalWord = totalFiles === 1 ? "file" : "files";
-               const stagedWord = completedCount === 1 ? "file" : "files";
-               summary.innerHTML = `<strong>${completedCount} of ${totalFiles} 
${totalWord} staged.</strong>
-                       <span class="ms-2">You can finalise with the staged 
files or retry failed uploads.</span>
-                       <button type="button" class="btn btn-primary ms-3" 
id="finalise-btn">Finalise ${completedCount} ${stagedWord}</button>`;
-               container.append(summary);
+       showProcessing() {
+               const statusEl = document.getElementById("upload-batch-status");
+               if (statusEl) statusEl.textContent = "Processing revision";
+               const cancelBtn = document.getElementById("upload-cancel-btn");
+               if (cancelBtn) cancelBtn.classList.add("d-none");
                document
-                       .getElementById("finalise-btn")
-                       .addEventListener("click", onFinalise);
+                       .querySelectorAll("#upload-file-list 
.upload-file-status")
+                       .forEach((el) => {
+                               el.textContent = "Done";
+                               el.className = "upload-file-status 
text-success";
+                       });
        },
 
-       doFinalise(container, finaliseUrl, csrfToken) {
-               const existingSummary = 
document.getElementById("upload-summary");
-               if (existingSummary) existingSummary.remove();
-               const summary = document.createElement("div");
-               summary.className = "alert alert-info mt-3";
-               summary.innerHTML = `<strong>Finalising upload...</strong>`;
-               container.append(summary);
-               const form = document.createElement("form");
-               form.method = "POST";
-               form.action = finaliseUrl;
-               form.style.display = "none";
-               const csrfInput = document.createElement("input");
-               csrfInput.type = "hidden";
-               csrfInput.name = "csrf_token";
-               csrfInput.value = csrfToken;
-               form.append(csrfInput);
-               document.body.append(form);
-               form.submit();
+       showSuccess(message) {
+               const statusEl = document.getElementById("upload-batch-status");
+               if (statusEl) statusEl.textContent = "Upload complete";
+               const cancelBtn = document.getElementById("upload-cancel-btn");
+               if (cancelBtn) cancelBtn.classList.add("d-none");
+               const progressBar = 
document.getElementById("upload-overall-progress");
+               if (progressBar) progressBar.value = 100;
+               const percentEl = 
document.getElementById("upload-progress-percent");
+               if (percentEl) percentEl.textContent = "100%";
+               const msgArea = document.getElementById("upload-message-area");
+               if (msgArea) {
+                       const alert = document.createElement("div");
+                       alert.className = "alert alert-success mt-3";
+                       alert.textContent = message;
+                       msgArea.append(alert);
+               }
        },
 
-       handleUploadProgress(e, progressBar, statusEl, percentEl, cancelBtn) {
-               if (e.lengthComputable) {
-                       const pct = Math.round((e.loaded / e.total) * 100);
-                       progressBar.value = pct;
-                       delete progressBar.dataset.indeterminate;
-                       percentEl.textContent = `${pct}%`;
-                       if (pct >= 100) {
-                               statusEl.textContent = "Processing...";
-                               cancelBtn.classList.add("d-none");
-                       } else {
-                               statusEl.textContent = 
`${this.formatBytes(e.loaded)} / ${this.formatBytes(e.total)}`;
-                       }
-               } else {
-                       progressBar.removeAttribute("value");
-                       progressBar.dataset.indeterminate = "true";
-                       percentEl.textContent = "";
-                       statusEl.textContent = `${this.formatBytes(e.loaded)} 
uploaded`;
+       showUploading(fileCount) {
+               const statusEl = document.getElementById("upload-batch-status");
+               if (statusEl) {
+                       const word = fileCount === 1 ? "file" : "files";
+                       statusEl.textContent = `Uploading ${fileCount} ${word}`;
+               }
+       },
+
+       updateProgress(loaded, total, cumulative, totalFileBytes) {
+               let pct = 0;
+               if (total > 0) {
+                       pct = loaded >= total ? 100 : Math.floor((loaded / 
total) * 100);
+               }
+               const progressBar = 
document.getElementById("upload-overall-progress");
+               const percentEl = 
document.getElementById("upload-progress-percent");
+               const bytesEl = 
document.getElementById("upload-progress-bytes");
+
+               if (progressBar) progressBar.value = pct;
+               if (percentEl) percentEl.textContent = `${pct}%`;
+               if (bytesEl) {
+                       bytesEl.textContent = `${this.formatBytes(loaded)} / 
${this.formatBytes(total)}`;
+               }
+
+               if (totalFileBytes > 0) {
+                       const scaled = loaded * (totalFileBytes / total);
+                       document
+                               .querySelectorAll("#upload-file-list 
[data-file-index]")
+                               .forEach((row, i) => {
+                                       const start = i > 0 ? cumulative[i - 1] 
: 0;
+                                       const end = cumulative[i];
+                                       const statusSpan = 
row.querySelector(".upload-file-status");
+                                       if (!statusSpan) return;
+                                       if (scaled >= end) {
+                                               statusSpan.textContent = "Done";
+                                               statusSpan.className = 
"upload-file-status text-success";
+                                       } else if (scaled > start) {
+                                               statusSpan.textContent = 
"Sending";
+                                               statusSpan.className = 
"upload-file-status text-primary";
+                                       } else {
+                                               statusSpan.textContent = 
"Pending";
+                                               statusSpan.className = 
"upload-file-status text-muted";
+                                       }
+                               });
                }
        },
 };
diff --git a/atr/static/js/src/upload-progress.js 
b/atr/static/js/src/upload-progress.js
index c1625502..1bc21949 100644
--- a/atr/static/js/src/upload-progress.js
+++ b/atr/static/js/src/upload-progress.js
@@ -17,227 +17,102 @@
  *  under the License.
  */
 
-function handleXhrLoad(xhr, ctx) {
-       ctx.activeUploads.delete(ctx.index);
-       if (xhr.status >= 200 && xhr.status < 300) {
-               UploadUI.markUploadDone(ctx.ui, true, "Staged");
-               ctx.state.completedCount++;
-       } else {
-               let msg = "Upload failed";
+(() => {
+       function computeCumulative(files) {
+               const cumulative = [];
+               let sum = 0;
+               for (const file of files) {
+                       sum += file.size;
+                       cumulative.push(sum);
+               }
+               return { cumulative, totalFileBytes: sum };
+       }
+
+       function handleLoad(xhr, retry) {
+               let data;
                try {
-                       msg = JSON.parse(xhr.responseText).error || msg;
+                       data = JSON.parse(xhr.responseText);
                } catch {
-                       /* ignore */
+                       UploadUI.showError(`Unexpected server response 
(${xhr.status})`, retry);
+                       return;
                }
-               UploadUI.markUploadDone(ctx.ui, false, `Failed: ${msg}`);
-               ctx.state.failedCount++;
-               ctx.addRetryButton(ctx.ui, ctx.index);
-       }
-       ctx.checkAllComplete();
-}
 
-function handleXhrFailure(ctx, reason, isCancelled) {
-       ctx.activeUploads.delete(ctx.index);
-       if (isCancelled) {
-               ctx.statusEl.textContent = "Cancelled";
-               ctx.statusEl.className = "upload-status text-secondary";
-               ctx.cancelBtn.classList.add("d-none");
-       } else {
-               UploadUI.markUploadDone(ctx.ui, false, `Failed: ${reason}`);
-               ctx.addRetryButton(ctx.ui, ctx.index);
+               if (data.ok) {
+                       UploadUI.showSuccess(data.message || "Upload complete");
+                       if (data.next_url) {
+                               window.location.assign(data.next_url);
+                       }
+               } else {
+                       UploadUI.showError(data.message || "Upload failed", 
retry);
+               }
        }
-       ctx.state.failedCount++;
-       ctx.checkAllComplete();
-}
 
-function setupXhrHandlers(
-       xhr,
-       ui,
-       index,
-       state,
-       addRetryButtonFn,
-       checkAllCompleteFn,
-) {
-       const ctx = {
-               ui,
-               index,
-               state,
-               addRetryButton: addRetryButtonFn,
-               checkAllComplete: checkAllCompleteFn,
-               activeUploads: state.activeUploads,
-               statusEl: ui.querySelector(".upload-status"),
-               cancelBtn: ui.querySelector(".cancel-btn"),
-       };
-       const progressBar = ui.querySelector("progress");
-       const percentEl = ui.querySelector(".upload-percent");
+       function startUpload(form, container, files, formData) {
+               const { cumulative, totalFileBytes } = computeCumulative(files);
+               let reachedFull = false;
+               const xhr = new XMLHttpRequest();
 
-       xhr.upload.addEventListener("progress", (e) =>
-               UploadUI.handleUploadProgress(
-                       e,
-                       progressBar,
-                       ctx.statusEl,
-                       percentEl,
-                       ctx.cancelBtn,
-               ),
-       );
-       xhr.addEventListener("load", () => handleXhrLoad(xhr, ctx));
-       xhr.addEventListener("error", () =>
-               handleXhrFailure(ctx, "Network error", false),
-       );
-       xhr.addEventListener("abort", () => handleXhrFailure(ctx, null, true));
-}
+               function retry() {
+                       form.classList.remove("d-none");
+                       container.classList.add("d-none");
+               }
 
-function checkAllComplete(state, container, finaliseUrl, csrfToken) {
-       if (state.activeUploads.size > 0) return;
-       if (state.completedCount === 0) {
-               UploadUI.showAllFailedSummary(container);
-       } else if (state.failedCount > 0) {
-               UploadUI.showPartialSuccessSummary(
-                       container,
-                       state.completedCount,
-                       state.totalFiles,
-                       () => UploadUI.doFinalise(container, finaliseUrl, 
csrfToken),
-               );
-       } else {
-               UploadUI.doFinalise(container, finaliseUrl, csrfToken);
-       }
-}
+               UploadUI.buildBatchUI(container, files, () => xhr.abort());
+               UploadUI.showUploading(files.length);
 
-function startUpload(
-       file,
-       index,
-       state,
-       stageUrl,
-       csrfToken,
-       addRetryButtonFn,
-       checkComplete,
-) {
-       const ui = document.getElementById(`upload-file-${index}`);
-       const xhr = new XMLHttpRequest();
-       state.activeUploads.set(index, xhr);
-       setupXhrHandlers(xhr, ui, index, state, addRetryButtonFn, 
checkComplete);
-       const formData = new FormData();
-       formData.append("csrf_token", csrfToken);
-       formData.append("file", file, file.name);
-       xhr.open("POST", stageUrl, true);
-       xhr.send(formData);
-       ui.querySelector(".upload-status").textContent = "Uploading...";
-}
+               xhr.upload.addEventListener("progress", (ev) => {
+                       if (ev.lengthComputable) {
+                               if (ev.loaded >= ev.total) {
+                                       reachedFull = true;
+                                       UploadUI.showProcessing();
+                               }
+                               UploadUI.updateProgress(
+                                       ev.loaded,
+                                       ev.total,
+                                       cumulative,
+                                       totalFileBytes,
+                               );
+                       }
+               });
 
-function addRetryButton(
-       ui,
-       index,
-       state,
-       fileInput,
-       stageUrl,
-       csrfToken,
-       checkComplete,
-) {
-       const retryBtn = document.createElement("button");
-       retryBtn.type = "button";
-       retryBtn.className = "btn btn-sm btn-outline-primary retry-btn ms-2";
-       retryBtn.textContent = "Retry";
-       retryBtn.addEventListener("click", () => {
-               UploadUI.resetUploadUI(ui);
-               retryBtn.remove();
-               state.failedCount--;
-               const file = Array.from(fileInput.files || [])[index];
-               startUpload(
-                       file,
-                       index,
-                       state,
-                       stageUrl,
-                       csrfToken,
-                       addRetryButton,
-                       checkComplete,
-               );
-       });
-       ui.querySelector(".upload-status").parentNode.append(retryBtn);
-}
+               xhr.addEventListener("load", () => handleLoad(xhr, retry));
 
-function handleFormSubmit(
-       e,
-       form,
-       fileInput,
-       container,
-       state,
-       stageUrl,
-       csrfToken,
-       checkComplete,
-) {
-       e.preventDefault();
-       const files = Array.from(fileInput.files || []);
-       if (files.length === 0) {
-               alert("Please select files to upload.");
-               return;
-       }
-       state.totalFiles = files.length;
-       state.completedCount = 0;
-       state.failedCount = 0;
-       form.classList.add("d-none");
-       container.classList.remove("d-none");
-       container.innerHTML = "";
-       files.forEach((file, index) => {
-               container.append(
-                       UploadUI.createFileProgressUI(file, index, 
state.activeUploads),
-               );
-               startUpload(
-                       file,
-                       index,
-                       state,
-                       stageUrl,
-                       csrfToken,
-                       (ui, idx) =>
-                               addRetryButton(
-                                       ui,
-                                       idx,
-                                       state,
-                                       fileInput,
-                                       stageUrl,
-                                       csrfToken,
-                                       checkComplete,
-                               ),
-                       checkComplete,
-               );
-       });
-}
+               xhr.addEventListener("error", () => {
+                       if (reachedFull) {
+                               UploadUI.showAmbiguousError();
+                       } else {
+                               UploadUI.showError("A network error occurred.", 
retry);
+                       }
+               });
 
-(() => {
-       const config = document.getElementById("upload-config");
-       if (!config) return;
+               xhr.addEventListener("abort", () => {
+                       UploadUI.showCancelled();
+                       form.classList.remove("d-none");
+               });
 
-       const stageUrl = config.dataset.stageUrl;
-       const finaliseUrl = config.dataset.finaliseUrl;
-       if (!stageUrl || !finaliseUrl) return;
+               xhr.open("POST", form.action, true);
+               xhr.setRequestHeader("Accept", "application/json");
+               xhr.send(formData);
+       }
 
        const form = document
                .querySelector('input[name="variant"][value="add_files"]')
                ?.closest("form");
        if (!form) return;
 
-       const fileInput = form.querySelector('input[type="file"]');
+       const fileInput = form.querySelector('input[name="file_data"]');
        const container = document.getElementById("upload-progress-container");
-       const csrfToken = form.querySelector('input[name="csrf_token"]')?.value;
-       const state = {
-               activeUploads: new Map(),
-               completedCount: 0,
-               failedCount: 0,
-               totalFiles: 0,
-       };
-
-       const checkComplete = () =>
-               checkAllComplete(state, container, finaliseUrl, csrfToken);
+       if (!fileInput || !container) return;
 
-       form.addEventListener("submit", (e) =>
-               handleFormSubmit(
-                       e,
-                       form,
-                       fileInput,
-                       container,
-                       state,
-                       stageUrl,
-                       csrfToken,
-                       checkComplete,
-               ),
-       );
+       form.addEventListener("submit", (e) => {
+               e.preventDefault();
+               const files = Array.from(fileInput.files || []);
+               if (files.length === 0) {
+                       alert("Please select files to upload.");
+                       return;
+               }
+               const formData = new FormData(form);
+               form.classList.add("d-none");
+               startUpload(form, container, files, formData);
+       });
 })();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to