Compare commits

..

1 Commits

Author SHA1 Message Date
Jamison Lahman
4e9f651d64 fix(favicon): include content type in link metadata 2026-03-17 17:13:06 -07:00
7 changed files with 84 additions and 193 deletions

View File

@@ -479,9 +479,7 @@ def is_zip_file(file: UploadFile) -> bool:
def upload_files(
files: list[UploadFile],
file_origin: FileOrigin = FileOrigin.CONNECTOR,
unzip: bool = True,
files: list[UploadFile], file_origin: FileOrigin = FileOrigin.CONNECTOR
) -> FileUploadResponse:
# Skip directories and known macOS metadata entries
@@ -504,46 +502,31 @@ def upload_files(
if seen_zip:
raise HTTPException(status_code=400, detail=SEEN_ZIP_DETAIL)
seen_zip = True
# Validate the zip by opening it (catches corrupt/non-zip files)
with zipfile.ZipFile(file.file, "r") as zf:
if unzip:
zip_metadata_file_id = save_zip_metadata_to_file_store(
zf, file_store
zip_metadata_file_id = save_zip_metadata_to_file_store(
zf, file_store
)
for file_info in zf.namelist():
if zf.getinfo(file_info).is_dir():
continue
if not should_process_file(file_info):
continue
sub_file_bytes = zf.read(file_info)
mime_type, __ = mimetypes.guess_type(file_info)
if mime_type is None:
mime_type = "application/octet-stream"
file_id = file_store.save_file(
content=BytesIO(sub_file_bytes),
display_name=os.path.basename(file_info),
file_origin=file_origin,
file_type=mime_type,
)
for file_info in zf.namelist():
if zf.getinfo(file_info).is_dir():
continue
if not should_process_file(file_info):
continue
sub_file_bytes = zf.read(file_info)
mime_type, __ = mimetypes.guess_type(file_info)
if mime_type is None:
mime_type = "application/octet-stream"
file_id = file_store.save_file(
content=BytesIO(sub_file_bytes),
display_name=os.path.basename(file_info),
file_origin=file_origin,
file_type=mime_type,
)
deduped_file_paths.append(file_id)
deduped_file_names.append(os.path.basename(file_info))
continue
# Store the zip as-is (unzip=False)
file.file.seek(0)
file_id = file_store.save_file(
content=file.file,
display_name=file.filename,
file_origin=file_origin,
file_type=file.content_type or "application/zip",
)
deduped_file_paths.append(file_id)
deduped_file_names.append(file.filename)
deduped_file_paths.append(file_id)
deduped_file_names.append(os.path.basename(file_info))
continue
# Since we can't render docx files in the UI,
@@ -630,10 +613,9 @@ def _fetch_and_check_file_connector_cc_pair_permissions(
@router.post("/admin/connector/file/upload", tags=PUBLIC_API_TAGS)
def upload_files_api(
files: list[UploadFile],
unzip: bool = True,
_: User = Depends(current_curator_or_admin_user),
) -> FileUploadResponse:
return upload_files(files, FileOrigin.OTHER, unzip=unzip)
return upload_files(files, FileOrigin.OTHER)
@router.get("/admin/connector/{connector_id}/files", tags=PUBLIC_API_TAGS)

View File

@@ -297,10 +297,6 @@ def index_batch_params(
class TestDocumentIndexOld:
"""Tests the old DocumentIndex interface."""
# TODO(ENG-3864)(andrei): Re-enable this test.
@pytest.mark.xfail(
reason="Flaky test: Retrieved chunks vary non-deterministically before and after changing user projects and personas. Likely a timing issue with the index being updated."
)
def test_update_single_can_clear_user_projects_and_personas(
self,
document_indices: list[DocumentIndex],

View File

@@ -1,109 +0,0 @@
import io
import zipfile
from unittest.mock import MagicMock
from unittest.mock import patch
from zipfile import BadZipFile
import pytest
from fastapi import UploadFile
from starlette.datastructures import Headers
from onyx.configs.constants import FileOrigin
from onyx.server.documents.connector import upload_files
def _create_test_zip() -> bytes:
"""Create a simple in-memory zip file containing two text files."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("file1.txt", "hello")
zf.writestr("file2.txt", "world")
return buf.getvalue()
def _make_upload_file(content: bytes, filename: str, content_type: str) -> UploadFile:
return UploadFile(
file=io.BytesIO(content),
filename=filename,
headers=Headers({"content-type": content_type}),
)
@patch("onyx.server.documents.connector.get_default_file_store")
def test_upload_zip_with_unzip_true_extracts_files(
mock_get_store: MagicMock,
) -> None:
"""When unzip=True (default), a zip upload is extracted into individual files."""
mock_store = MagicMock()
mock_store.save_file.side_effect = lambda **kwargs: f"id-{kwargs['display_name']}"
mock_get_store.return_value = mock_store
zip_bytes = _create_test_zip()
upload = _make_upload_file(zip_bytes, "test.zip", "application/zip")
result = upload_files([upload], FileOrigin.CONNECTOR)
# Should have extracted the two individual files, not stored the zip itself
assert len(result.file_paths) == 2
assert "id-file1.txt" in result.file_paths
assert "id-file2.txt" in result.file_paths
assert "file1.txt" in result.file_names
assert "file2.txt" in result.file_names
@patch("onyx.server.documents.connector.get_default_file_store")
def test_upload_zip_with_unzip_false_stores_zip_as_is(
mock_get_store: MagicMock,
) -> None:
"""When unzip=False, the zip file is stored as-is without extraction."""
mock_store = MagicMock()
mock_store.save_file.return_value = "zip-file-id"
mock_get_store.return_value = mock_store
zip_bytes = _create_test_zip()
upload = _make_upload_file(zip_bytes, "site_export.zip", "application/zip")
result = upload_files([upload], FileOrigin.CONNECTOR, unzip=False)
# Should store exactly one file (the zip itself)
assert len(result.file_paths) == 1
assert result.file_paths[0] == "zip-file-id"
assert result.file_names == ["site_export.zip"]
# No zip metadata should be created
assert result.zip_metadata_file_id is None
# Verify the stored content is a valid zip
saved_content: io.BytesIO = mock_store.save_file.call_args[1]["content"]
saved_content.seek(0)
with zipfile.ZipFile(saved_content, "r") as zf:
assert set(zf.namelist()) == {"file1.txt", "file2.txt"}
@patch("onyx.server.documents.connector.get_default_file_store")
def test_upload_invalid_zip_with_unzip_false_raises(
mock_get_store: MagicMock,
) -> None:
"""An invalid zip is rejected even when unzip=False (validation still runs)."""
mock_get_store.return_value = MagicMock()
bad_zip = _make_upload_file(b"not a zip", "bad.zip", "application/zip")
with pytest.raises(BadZipFile):
upload_files([bad_zip], FileOrigin.CONNECTOR, unzip=False)
@patch("onyx.server.documents.connector.get_default_file_store")
def test_upload_multiple_zips_rejected_when_unzip_false(
mock_get_store: MagicMock,
) -> None:
"""The seen_zip guard rejects a second zip even when unzip=False."""
mock_store = MagicMock()
mock_store.save_file.return_value = "zip-id"
mock_get_store.return_value = mock_store
zip_bytes = _create_test_zip()
zip1 = _make_upload_file(zip_bytes, "a.zip", "application/zip")
zip2 = _make_upload_file(zip_bytes, "b.zip", "application/zip")
with pytest.raises(Exception, match="Only one zip file"):
upload_files([zip1, zip2], FileOrigin.CONNECTOR, unzip=False)

View File

@@ -21,13 +21,10 @@ export const submitGoogleSite = async (
formData.append("files", file);
});
const response = await fetch(
"/api/manage/admin/connector/file/upload?unzip=false",
{
method: "POST",
body: formData,
}
);
const response = await fetch("/api/manage/admin/connector/file/upload", {
method: "POST",
body: formData,
});
const responseJson = await response.json();
if (!response.ok) {
toast.error(`Unable to upload files - ${responseJson.detail}`);

View File

@@ -4,6 +4,7 @@ import {
fetchEnterpriseSettingsSS,
fetchSettingsSS,
} from "@/components/settings/lib";
import { fetchSS } from "@/lib/utilsSS";
import {
CUSTOM_ANALYTICS_ENABLED,
GTM_ENABLED,
@@ -55,11 +56,28 @@ export async function generateMetadata(): Promise<Metadata> {
: "/onyx.ico";
}
const useCustomLogo =
enterpriseSettings && enterpriseSettings.use_custom_logo;
let logoMimeType: string = "image/png";
if (useCustomLogo) {
try {
const logoRes = await fetchSS("/enterprise-settings/logo", {
method: "HEAD",
});
logoMimeType = logoRes.headers.get("content-type") || "image/png";
} catch {
// Fall back to image/png if the HEAD request fails
}
}
return {
title: enterpriseSettings?.application_name || "Onyx",
description: "Question answering for your documents",
icons: {
icon: logoLocation,
icon: useCustomLogo
? { url: logoLocation, type: logoMimeType }
: { url: logoLocation, type: "image/x-icon" },
},
};
}

View File

@@ -12,7 +12,6 @@ import {
SvgKey,
} from "@opal/icons";
import { Disabled } from "@opal/core";
import LineItem from "@/refresh-components/buttons/LineItem";
import Popover from "@/refresh-components/Popover";
import Separator from "@/refresh-components/Separator";
import { Section } from "@/layouts/general-layouts";
@@ -79,17 +78,18 @@ export default function UserRowActions({
return (
<>
{user.id && (
<LineItem
<Button
prominence="tertiary"
icon={SvgUsers}
onClick={() => openModal(Modal.EDIT_GROUPS)}
>
Groups &amp; Roles
</LineItem>
</Button>
)}
<Disabled disabled>
<LineItem danger icon={SvgUserX}>
<Button prominence="tertiary" variant="danger" icon={SvgUserX}>
Deactivate User
</LineItem>
</Button>
</Disabled>
<Separator paddingXRem={0.5} />
<Text as="p" secondaryBody text03 className="px-3 py-1">
@@ -102,18 +102,20 @@ export default function UserRowActions({
switch (user.status) {
case UserStatus.INVITED:
return (
<LineItem
danger
<Button
prominence="tertiary"
variant="danger"
icon={SvgXCircle}
onClick={() => openModal(Modal.CANCEL_INVITE)}
>
Cancel Invite
</LineItem>
</Button>
);
case UserStatus.REQUESTED:
return (
<LineItem
<Button
prominence="tertiary"
icon={SvgUserCheck}
onClick={() => {
setPopoverOpen(false);
@@ -131,34 +133,37 @@ export default function UserRowActions({
}}
>
Approve
</LineItem>
</Button>
);
case UserStatus.ACTIVE:
return (
<>
{user.id && (
<LineItem
<Button
prominence="tertiary"
icon={SvgUsers}
onClick={() => openModal(Modal.EDIT_GROUPS)}
>
Groups &amp; Roles
</LineItem>
</Button>
)}
<LineItem
<Button
prominence="tertiary"
icon={SvgKey}
onClick={() => openModal(Modal.RESET_PASSWORD)}
>
Reset Password
</LineItem>
</Button>
<Separator paddingXRem={0.5} />
<LineItem
danger
<Button
prominence="tertiary"
variant="danger"
icon={SvgUserX}
onClick={() => openModal(Modal.DEACTIVATE)}
>
Deactivate User
</LineItem>
</Button>
</>
);
@@ -166,34 +171,38 @@ export default function UserRowActions({
return (
<>
{user.id && (
<LineItem
<Button
prominence="tertiary"
icon={SvgUsers}
onClick={() => openModal(Modal.EDIT_GROUPS)}
>
Groups &amp; Roles
</LineItem>
</Button>
)}
<LineItem
<Button
prominence="tertiary"
icon={SvgKey}
onClick={() => openModal(Modal.RESET_PASSWORD)}
>
Reset Password
</LineItem>
</Button>
<Separator paddingXRem={0.5} />
<LineItem
<Button
prominence="tertiary"
icon={SvgUserPlus}
onClick={() => openModal(Modal.ACTIVATE)}
>
Activate User
</LineItem>
</Button>
<Separator paddingXRem={0.5} />
<LineItem
danger
<Button
prominence="tertiary"
variant="danger"
icon={SvgUserX}
onClick={() => openModal(Modal.DELETE)}
>
Delete User
</LineItem>
</Button>
</>
);

View File

@@ -169,9 +169,7 @@ test.describe("Project Files visual regression", () => {
.first();
await expect(iconWrapper).toBeVisible();
const container = page.locator("[data-main-container]");
await expect(container).toBeVisible();
await expectElementScreenshot(container, {
await expectElementScreenshot(filesSection, {
name: "project-files-long-underscore-filename",
});