diff --git a/launchable/commands/record/attachment.py b/launchable/commands/record/attachment.py index b62fc89da..8740166ac 100644 --- a/launchable/commands/record/attachment.py +++ b/launchable/commands/record/attachment.py @@ -63,9 +63,10 @@ def attachment( [zip_info.filename, AttachmentStatus.SKIPPED_NON_TEXT]) continue + file_name = normalize_filename(zip_info.filename) status = post_attachment( - client, session, file_content, zip_info.filename) - summary_rows.append([zip_info.filename, status]) + client, session, file_content, file_name) + summary_rows.append([file_name, status]) # If tar file (tar, tar.gz, tar.bz2, tgz, etc.) elif tarfile.is_tarfile(a): @@ -88,9 +89,10 @@ def attachment( [tar_info.name, AttachmentStatus.SKIPPED_NON_TEXT]) continue + file_name = normalize_filename(tar_info.name) status = post_attachment( - client, session, file_content, tar_info.name) - summary_rows.append([tar_info.name, status]) + client, session, file_content, file_name) + summary_rows.append([file_name, status]) else: with open(a, mode='rb') as f: @@ -101,8 +103,9 @@ def attachment( [a, AttachmentStatus.SKIPPED_NON_TEXT]) continue - status = post_attachment(client, session, file_content, a) - summary_rows.append([a, status]) + file_name = normalize_filename(a) + status = post_attachment(client, session, file_content, file_name) + summary_rows.append([file_name, status]) except Exception as e: client.print_exception_and_recover(e) @@ -125,6 +128,13 @@ def matches_include_patterns(filename: str, include_patterns: Tuple[str, ...]) - return False +def normalize_filename(filename: str) -> str: + """ + Normalize filename by replacing whitespace with dashes. + """ + return filename.replace(' ', '-') + + def valid_utf8_file(file_content: bytes) -> bool: # Check for null bytes (binary files) if b'\x00' in file_content: diff --git a/tests/commands/record/test_attachment.py b/tests/commands/record/test_attachment.py index 38f8fe609..d77a0991b 100644 --- a/tests/commands/record/test_attachment.py +++ b/tests/commands/record/test_attachment.py @@ -161,3 +161,32 @@ def test_attachment_with_include_filter(self): | nested/debug.log | ✓ Recorded successfully | """ self.assertIn(expect, result.output) + + @responses.activate + @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_attachment_with_whitespace_in_filename(self): + TEST_CONTENT = b"Test log content" + + # emulate launchable record build & session + write_session(self.build_name, self.session_id) + + # Create a file with whitespace in the name + with tempfile.TemporaryDirectory() as temp_dir: + file_path = os.path.join(temp_dir, "app log file.txt") + with open(file_path, 'wb') as f: + f.write(TEST_CONTENT) + + # Expect the filename with whitespace replaced by dashes in the Content-Disposition header + responses.add( + responses.POST, + "{}/intake/organizations/{}/workspaces/{}/builds/{}/test_sessions/{}/attachment".format( + get_base_url(), self.organization, self.workspace, self.build_name, self.session_id), + match=[responses.matchers.header_matcher( + {"Content-Disposition": 'attachment;filename="{}"'.format(file_path.replace(' ', '-'))} + )], + status=200) + + result = self.cli("record", "attachment", "--session", self.session, file_path) + + self.assert_success(result) + self.assertIn("✓ Recorded successfully", result.output)