API를 통한 업로드 삭제

그것을 시도해 보았지만, 업로드를 삭제하는 API 엔드포인트가 존재하지 않는 것으로 보입니다. 존재하지 않음.

Muppet Show: Kermit's Trapeze Fall

이와 관련된 언급이나 제안이 전혀 없었기 때문에 여기에 게시합니다. 제가 찾은 가장 가까운 내용은 이 것입니다: API: a scope with access to /uploads

업로드의 API 키 세분화 범위 설정에서 'create’는 사용 가능하지만 'delete’는 사용할 수 없음을 확인했습니다.

잠재적인 우회 방법은 커스텀 자동화를 생성하고 이를 API를 통해 트리거하는 것일 수 있습니다.

4개의 좋아요

사용 사례는 무엇인가요?

설정 방식에 따라 업로드된 파일은 게시물, 사용자 프로필, 배지 등 여러 곳에 참조되는 공유 리소스가 될 수 있습니다. 따라서 적절한 검토 없이 삭제하는 것은 다소 위험할 수 있습니다.

또한, 어디에도 참조되지 않는 경우 일정 시간이 지나면 자동으로 정리됩니다.

3개의 좋아요

이 사용 사례는 activepieces, data explorer 및 API를 활용한 인간이 개입하는 인라인(human in the loop) 자동화 워크플로우의 일환으로, 시간적으로 민감한 업로드 삭제를 수행하는 것입니다.

그 목표는 SSH 접근 권한 없이도 일반 모더레이터가 즉시 업로드를 완전히 삭제할 수 있는 간단한 방법을 제공하여, 업로드 참조가 깨지는 모든 알려진 경우를 포괄적으로 처리하고, 특정 URL(프록시된 아바타의 경우 사용자 이름 기반 URL 접두사 포함)에 대해 CDN을 자동으로 정리(purge)할 수 있도록 하는 것입니다.


테스트를 해본 결과, 업로드가 파괴(destroy)될 때 아바타, 프로필 배경, 카드 배경 참조가 자동으로 처리되는 것을 확인하여 안도했습니다.

https://github.com/discourse/discourse/commit/e1975e293f2625259e925b4a3c93d88d5acfcaa8

https://github.com/discourse/discourse/commit/38e7b1a0492dd4282c3cd3b1ddb2b3343661d31f


주요 시나리오는 두 가지로, ‘화약고(Scorched earth)’ 방식과 ‘정밀 제거(Surgical removal)’ 방식입니다. 다음은 화약고 방식에 대한 진행 중인 작업 내용입니다:


업로드 해시 목록으로 변환하기:

  • 아바타 URL (정규식으로 사용자 이름 추출) → data explorer 쿼리를 통해 아바타 해시 가져오기 (URL에는 해시가 포함되지 않음)

  • 토픽/게시물 URL → data explorer를 사용하여 해당 게시물에서 사용된 모든 업로드 해시 수집

  • 직접적인 원본/최적화 업로드 URL (프로필 배경 및 카드 배경 포함) → 정규식으로 해시 추출


그 다음 각 해시를 쿼리하여 모든 발생 위치를 찾습니다 (모든 경우를 포괄하는 하나의 data explorer 쿼리, 한 번에 하나의 해시):

  • 아바타로 사용한 사용자 목록 (사용자 이름/ID)

  • 프로필 배경으로 사용한 사용자 목록 (사용자 이름/ID)

  • 카드 배경으로 사용한 사용자 목록 (사용자 이름/ID)

  • 이 업로드를 사용하는 모든 게시물(원본) 목록


실행 작업:

  • 해당 업로드를 아바타, 프로필 배경 또는 카드 배경으로 사용한 모든 사용자 정지(suspend)

  • 대상 업로드를 참조하는 게시물이 있는 모든 사용자 정지 (단, 인용(quote) 안에 중첩된 참조는 제외)

  • 모든 토픽 삭제

  • 모든 게시물 삭제

  • 업로드 파괴 (API 엔드포인트 없음)

  • 모든 CDN URL 정리 (최적화/비최적화)

  • 각 관련 사용자 이름에 대해 프록시된 아바타 URL의 표준 접두사 정리 (모든 크기 커버링)


정밀 제거 시나리오는 본질적으로 동일하지만, 관련 사용자를 정지하지 않으며, 게시물/토픽 URL에서 모든 업로드 해시를 수집하는 방식에 일부 변경이 필요합니다.

깨진 참조를 피하기 위해 게시물/토픽 자체를 삭제하는 것이 여전히 필요할 가능성이 높지만, 해당 특정 업로드의 마크다운만 제거하고(다른 업로드에는 영향을 주지 않고) 다른 업로드에는 손대지 않는 방식이 가능하다면 더 우수할 것입니다. 모든 마크다운 업로드 참조를 제거하지 않는다면, 이 자동화와 같은 방식이 될 것입니다.


이상적으로는 해시 자체를 차단할 수 있으면 좋겠습니다. 위에서 설명한 과정을 거친 후에도 누군가가 단순히 새 계정을 만들고 재업로드하지 못하도록 하기 위해서입니다. :melting_face: :coffin:

현재 일반 모더레이터가 감시 단어(watched words) 등을 사용하여 이를 수행하는 것은 불가능하다고 생각합니다. 따라서 해시 목록에 대해 주기적으로 위와 같은 스캔을 수행하는 것이 이를 처리하는 방법이 될 수 있습니다.

1개의 좋아요

Legal Compliance Plugin 을 확인해 보셨나요?

이 플러그인은 게시물의 업로드를 가져오고, 업로드 ID와 게시물 ID를 지정하여 업로드를 삭제하는 엔드포인트를 제공합니다.

또한, 해시를 기반으로 특정 업로드를 포함하는 모든 게시물을 검색할 수 있도록 검색 기능을 확장했습니다.

3개의 좋아요

이 게시물을 읽고 동의하는 사용자들을 위해 이 링크를 공유합니다. 이 안건에 투표하는 데 관심이 있을 수 있으니 참고해 주세요(이미 읽으셨을 것 같지만):

2개의 좋아요

네! 지난 달에 이 플러그인을 보고 거의 기쁨의 눈물을 흘릴 뻔했어요. ㅋㅋ 만들고 공유해 주셔서 정말 감사합니다. :heart:

이 플러그인으로 처리할 수 없었던 몇 가지 시나리오가 있습니다:

  • upload: 검색 접두어를 사용해도 프로필 배경이나 카드 배경을 찾지 못했습니다(다른 사람이 게시물에 해당 이미지를 직접 링크한 경우를 제외하고).
  • 게시물과 연관성이 없는 이미지(해당 시나리오)를 영구 삭제하는 것.
  • 아바타의 경우에도 유사합니다. 예를 들어 아바타의 해시를 가져오기(보통 URL에 포함되지 않음), 그리고 필요 시 해당 아바타를 사용했을 가능성이 있는 다른 계정을 찾아서 정지 조치하고, 게시물 연관성이 없는 업로드를 삭제하는 것.

있으면 좋은 기능들:

  • 업로드 삭제 시 웹훅을 트리거하여, 업로드의 모든 변형에 대해 CDN 정화를 수행하는 것.
  • 업로드가 삭제될 때 해당 업로드 참조가 있는 모든 게시물/주제를 일괄/자동으로 삭제하는 것.

Discourse(웹 UI)에서는 관리자/모드레이터나 사용자 본인이 사용자의 아바타를 삭제(연결 해제)할 수 없는 것 같습니다(대체 이미지를 업로드하는 경우를 제외하고). 프로필 배경과 배너는 가능합니다. 하지만 이 둘 모두 업로드를 즉시 파괴하지 않으며, 다른 사용자가 자신의 프로필(아바타, 프로필 배경, 프로필 카드)의 일부로 동일한 업로드를 사용 중인 경우 나중에 정화되지도 않습니다.


결국 대부분의 프로세스(또는 전체 프로세스)를 처리할 수 있는 자동화 워크플로우를 만들어 보는 것이 가치 있을 것 같다고 생각하게 되었습니다(인간의 검토/승인 및/또는 단순화된 인간 입력을 포함하여). 일관되게 적용하기 쉽고, 엣지 케이스를 커버하며, 이상적으로는 업로드 참조가 죽어 있는(존재하지 않는) 게시물이 남지 않도록 가능성을 최소화하기 위함입니다. 또한 필요 시 자동 정지(목표 업로드가 인용문 안에 있는 경우를 제외하고) 및 CDN 정화도 수행합니다.


현재까지 작성한 데이터 탐색기(Data Explorer) 쿼리는 다음과 같습니다(아직 ‘그들을 하나로 묶는’ 단계는 아님). 목표는 다음을 커버하는 것입니다:

  • 주제 URL
  • 게시물 URL
  • 직접 이미지 URL (게시물, 프로필 배경, 카드 배경)
  • 아바타 URL (프록시된)

업로드 해시 목록 준비

사용자명 (프록시된 아바타 URL에서) 에서 업로드 해시로

-- [params]
-- user_id :username
SELECT
    up.sha1 AS upload_hash
FROM
    users u
    JOIN uploads up ON up.id = u.uploaded_avatar_id
WHERE
    u.id = :username

게시물/주제 URL 에서 업로드 해시 목록으로

-- [params]
-- post_id :post_url
SELECT
    COALESCE(json_agg(up.sha1), '[]'::json) AS upload_hashes
FROM
    upload_references ur
    JOIN uploads up ON up.id = ur.upload_id
WHERE
    ur.target_id = :post_url
    AND ur.target_type = 'Post'

제가 알고 있는 다른 업로드 URL 유형들은 URL 자체에 해시가 포함되어 있습니다(해시 가져오기 위해 데이터 탐색기를 사용할 필요가 없음).


업로드 해시에 대한 모든 실행 가능한 정보 수집 및 준비

예시 매개변수:

upload_schemeless_prefix:
//my-s3-bucket.somehost.example.com

cdn_url_prefix:
https://cdn.example.com

app_hostname:
www.example.com

upload_hash:
0000000000000000000000000000000000000000

해시 검색 및 요약

-- [params]
-- string :upload_hash
-- string :upload_schemeless_prefix
-- string :cdn_url_prefix
-- string :app_hostname
WITH target_uploads AS (
    SELECT
        id,
        url,
        user_id
    FROM
        uploads
    WHERE
        sha1 = :upload_hash
),
all_urls AS (
    SELECT
        url
    FROM
        target_uploads
    UNION
    SELECT
        url
    FROM
        optimized_images
    WHERE
        upload_id IN (
            SELECT
                id
            FROM
                target_uploads)
),
post_data AS (
    SELECT
        p.id AS post_id,
        p.topic_id,
        u.id AS user_id,
        u.username AS author,
        p.created_at,
        CASE WHEN p.post_number = 1 THEN
            TRUE
        ELSE
            FALSE
        END AS is_topic_starter,
        CASE WHEN t.archetype = 'private_message' THEN
            TRUE
        ELSE
            FALSE
        END AS is_dm,
        p.raw
    FROM
        upload_references ur
        JOIN posts p ON p.id = ur.target_id
            AND ur.target_type = 'Post'
        JOIN topics t ON t.id = p.topic_id
        JOIN users u ON u.id = p.user_id
    WHERE
        ur.upload_id IN (
            SELECT
                id
            FROM
                target_uploads)
            AND p.deleted_at IS NULL
)
SELECT
    (
        SELECT
            COALESCE(json_agg(id), '[]'::json)
        FROM
            target_uploads) AS upload_ids,
    (
        SELECT
            COALESCE(json_agg(url), '[]'::json)
        FROM
            all_urls) AS upload_s3_schemeless_urls,
    (
        SELECT
            COALESCE(json_agg(
                    CASE WHEN url LIKE '//%' THEN
                        REPLACE(url, :upload_schemeless_prefix, :cdn_url_prefix)
                    ELSE
                        :cdn_url_prefix || url
                    END), '[]'::json)
        FROM
            all_urls) AS upload_cdn_urls,
    (
        SELECT
            COALESCE(json_agg(json_build_object('user_id', id, 'username', username)), '[]'::json)
        FROM
            users
        WHERE
            uploaded_avatar_id IN (
                SELECT
                    id
                FROM
                    target_uploads)) AS avatar_users,
    (
        SELECT
            COALESCE(json_agg('https://' || :app_hostname || '/user_avatar/' || :app_hostname || '/' || username || '/'), '[]'::json)
        FROM
            users
        WHERE
            uploaded_avatar_id IN (
                SELECT
                    id
                FROM
                    target_uploads)) AS avatar_proxied_url_prefixes,
    (
        SELECT
            COALESCE(json_agg(json_build_object('user_id', u.id, 'username', u.username)), '[]'::json)
        FROM
            user_profiles up
            JOIN users u ON u.id = up.user_id
        WHERE
            up.profile_background_upload_id IN (
                SELECT
                    id
                FROM
                    target_uploads)) AS profile_background_users,
    (
        SELECT
            COALESCE(json_agg(json_build_object('user_id', u.id, 'username', u.username)), '[]'::json)
        FROM
            user_profiles up
            JOIN users u ON u.id = up.user_id
        WHERE
            up.card_background_upload_id IN (
                SELECT
                    id
                FROM
                    target_uploads)) AS card_background_users,
    (
        SELECT
            COALESCE(json_agg(pd.topic_id), '[]'::json)
        FROM
            post_data pd
        WHERE
            pd.is_topic_starter = TRUE) AS topic_ids,
    (
        SELECT
            COALESCE(json_agg(pd.post_id), '[]'::json)
        FROM
            post_data pd
        WHERE
            pd.is_topic_starter = FALSE) AS post_ids,
    (
        SELECT
            COALESCE(json_agg(json_build_object('post_id', pd.post_id, 'topic_id', pd.topic_id, 'user_id', pd.user_id, 'author', pd.author, 'created_at', pd.created_at, 'is_topic_starter', pd.is_topic_starter, 'is_dm', pd.is_dm, 'raw', pd.raw)), '[]'::json)
        FROM
            post_data pd) AS post_details

커버하지 않는 항목: 검토 대기열의 주제/게시물 또는 초안 게시물

출력:

upload_ids (단 하나여야 함)
upload_s3_schemeless_urls
upload_cdn_urls
avatar_users
avatar_proxied_url_prefixes
profile_background_users
card_background_users
topic_ids
post_ids
post_details

이를 통해 다음 API를 선택적으로 사용할 수 있는 필요한 정보를 제공합니다:

  • 주제 삭제
  • 단일 게시물 삭제
  • 사용자 정지 (게시물의 경우, post_details의 raw를 사용하여 업로드 마크다운이 인용문 안에 있는 경우 사용자를 조건부로 제외할 수 있음)
  • :cross_mark: 업로드 삭제 (현재 API가 존재하지 않음)

그리고 추가 조치로:

  • 모든 CDN URL 정화 (모든 변형, 최적화 및 원본 등)
  • 프록시된 아바타의 접두어 URL 정화 (모든 크기를 커버하기 위해)

Discourse가 자동으로 처리하는 항목:

  • 업로드가 파괴/삭제되면 즉시 모든 사용자 프로필(아바타, 프로필 배경 및 카드 배경)에서 업로드 참조를 제거합니다.
1개의 좋아요