Repository: conan-io/conan

Issue:
[feature] show uploaded files size in `conan upload`
<!-- What is your suggestion? Please be as specific as possible! -->
It would be very convenient to provide the size of files which will be upload to specific remote via `conan upload ...` in diagnostic purposes, e.g. sometimes artifacts could be very large and CI could refuse its uploading, see e.g. [this StackOverflow question](https://stackoverflow.com/questions/64329087/gitlab-self-hosted-error-uploading-artifacts-as-archive-to-coordinator-to) as example of related CI error. With this change CI administrators could adjust the file limit at CI without trial and error changing of CI configs.

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).



Discussion:
It is relatively straightforward to just check the files in the Conan cache in case the upload failed, so not a high priority. We might add it to the output in Conan 2.0, depends on how cluttered the output looks, might be added or not, we will see.
@memsharded was the decision to add it or to not? Should this ticket be closed?
Neither, a decision was not taken. This kind of small ux details haven't been considered yet. I am fine with adding the info before/during upload of big files only (all files would be very noisy)



Patch:
diff --git a/conans/client/cmd/uploader.py b/conans/client/cmd/uploader.py
index 65afb5150f5..424e100ff9b 100644
--- a/conans/client/cmd/uploader.py
+++ b/conans/client/cmd/uploader.py
@@ -9,7 +9,8 @@
 from conans.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME,
                           EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO)
 from conans.util.files import (clean_dirty, is_dirty, gather_files,
-                               gzopen_without_timestamps, set_dirty_context_manager, mkdir)
+                               gzopen_without_timestamps, set_dirty_context_manager, mkdir,
+                               human_size)
 
 UPLOAD_POLICY_FORCE = "force-upload"
 UPLOAD_POLICY_SKIP = "skip-upload"
@@ -220,10 +221,11 @@ def upload(self, upload_data, remote):
 
     def upload_recipe(self, ref, bundle, remote):
         output = ConanOutput(scope=str(ref))
-        output.info(f"Uploading recipe '{ref.repr_notime()}'")
-        t1 = time.time()
         cache_files = bundle["files"]
 
+        output.info(f"Uploading recipe '{ref.repr_notime()}' ({_total_size(cache_files)})")
+
+        t1 = time.time()
         self._app.remote_manager.upload_recipe(ref, cache_files, remote)
 
         duration = time.time() - t1
@@ -232,11 +234,12 @@ def upload_recipe(self, ref, bundle, remote):
 
     def upload_package(self, pref, prev_bundle, remote):
         output = ConanOutput(scope=str(pref.ref))
-        output.info(f"Uploading package '{pref.repr_notime()}'")
         cache_files = prev_bundle["files"]
         assert (pref.revision is not None), "Cannot upload a package without PREV"
         assert (pref.ref.revision is not None), "Cannot upload a package without RREV"
 
+        output.info(f"Uploading package '{pref.repr_notime()}' ({_total_size(cache_files)})")
+
         t1 = time.time()
         self._app.remote_manager.upload_package(pref, cache_files, remote)
         duration = time.time() - t1
@@ -259,3 +262,11 @@ def compress_files(files, name, dest_dir, compresslevel=None, ref=None):
     duration = time.time() - t1
     ConanOutput().debug(f"{name} compressed in {duration} time")
     return tgz_path
+
+
+def _total_size(cache_files):
+    total_size = 0
+    for file in cache_files.values():
+        stat = os.stat(file)
+        total_size += stat.st_size
+    return human_size(total_size)


Test patch:
diff --git a/conans/test/integration/command/source_test.py b/conans/test/integration/command/source_test.py
index d6e6a83933b..57644154ed2 100644
--- a/conans/test/integration/command/source_test.py
+++ b/conans/test/integration/command/source_test.py
@@ -1,4 +1,5 @@
 import os
+import re
 import textwrap
 import unittest
 from collections import OrderedDict
@@ -156,6 +157,9 @@ def test_retrieve_exports_sources(self):
         client.run("create . --name=hello --version=0.1")
         rrev = client.exported_recipe_revision()
         client.run("upload hello/0.1 -r server0")
+        # Ensure we uploaded it
+        assert re.search(r"Uploading recipe 'hello/0.1#.*' \(.*\)", client.out)
+        assert re.search(r"Uploading package 'hello/0.1#.*' \(.*\)", client.out)
         client.run("remove * -c")
 
         # install from server0 that has the sources, upload to server1 (does not have the package)