fix: Store Linear workspace logo in storage (#9061)

* fix: Store Linear workspace logo in Outline

* use async task

* Move task into plugin

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
Hemachandar
2025-04-26 08:48:21 +05:30
committed by GitHub
parent 3156f62e94
commit e8e46a438c
4 changed files with 77 additions and 5 deletions
+11 -1
View File
@@ -8,6 +8,7 @@ import validate from "@server/middlewares/validate";
import { IntegrationAuthentication, Integration, Team } from "@server/models";
import { APIContext } from "@server/types";
import { Linear } from "../linear";
import UploadLinearWorkspaceLogoTask from "../tasks/UploadLinearWorkspaceLogoTask";
import * as T from "./schema";
import { LinearUtils } from "plugins/linear/shared/LinearUtils";
@@ -82,7 +83,9 @@ router.get(
},
{ transaction }
);
await Integration.create<Integration<IntegrationType.Embed>>(
const integration = await Integration.create<
Integration<IntegrationType.Embed>
>(
{
service: IntegrationService.Linear,
type: IntegrationType.Embed,
@@ -103,6 +106,13 @@ router.get(
{ transaction }
);
transaction.afterCommit(async () => {
await UploadLinearWorkspaceLogoTask.schedule({
integrationId: integration.id,
logoUrl: workspace.logoUrl,
});
});
ctx.redirect(LinearUtils.successUrl());
}
);
+5
View File
@@ -4,6 +4,7 @@ import config from "../plugin.json";
import router from "./api/linear";
import env from "./env";
import { Linear } from "./linear";
import UploadLinearWorkspaceLogoTask from "./tasks/UploadLinearWorkspaceLogoTask";
import { uninstall } from "./uninstall";
const enabled = !!env.LINEAR_CLIENT_ID && !!env.LINEAR_CLIENT_SECRET;
@@ -15,6 +16,10 @@ if (enabled) {
type: Hook.API,
value: router,
},
{
type: Hook.Task,
value: UploadLinearWorkspaceLogoTask,
},
{
type: Hook.UnfurlProvider,
value: { unfurl: Linear.unfurl, cacheExpiry: Minute.seconds },
@@ -0,0 +1,52 @@
import { v4 as uuidv4 } from "uuid";
import { IntegrationService, IntegrationType } from "@shared/types";
import { Integration } from "@server/models";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
import BaseTask, { TaskPriority } from "@server/queues/tasks/BaseTask";
import FileStorage from "@server/storage/files";
type Props = {
/** The integrationId to operate on */
integrationId: string;
/** The original logoUrl from Linear */
logoUrl: string;
};
/**
* A task that uploads the provided logoUrl to storage and updates the
* Linear integration record with the new url.
*/
export default class UploadLinearWorkspaceLogoTask extends BaseTask<Props> {
public async perform(props: Props) {
const integration = await Integration.scope("withAuthentication").findByPk<
Integration<IntegrationType.Embed>
>(props.integrationId);
if (!integration || integration.service !== IntegrationService.Linear) {
return;
}
const res = await FileStorage.storeFromUrl(
props.logoUrl,
`${Buckets.avatars}/${integration.teamId}/${uuidv4()}`,
"public-read",
{
headers: {
Authorization: `Bearer ${integration.authentication.token}`,
},
}
);
if (res?.url) {
integration.settings.linear!.workspace.logoUrl = res.url;
integration.changed("settings", true);
await integration.save();
}
}
public get options() {
return {
attempts: 3,
priority: TaskPriority.Normal,
};
}
}
+9 -4
View File
@@ -1,6 +1,7 @@
import { Blob } from "buffer";
import { Readable } from "stream";
import { PresignedPost } from "@aws-sdk/s3-presigned-post";
import omit from "lodash/omit";
import FileHelper from "@shared/editor/lib/FileHelper";
import { isBase64Url, isInternalUrl } from "@shared/utils/urls";
import env from "@server/env";
@@ -162,6 +163,12 @@ export default abstract class BaseStorage {
buffer = Buffer.from(match[2], "base64");
} else {
try {
const headers = {
"User-Agent": chromeUserAgent,
...init?.headers,
};
const initWithoutHeaders = omit(init, ["headers"]);
const res = await fetch(url, {
follow: 3,
redirect: "follow",
@@ -169,11 +176,9 @@ export default abstract class BaseStorage {
options?.maxUploadSize ?? Infinity,
env.FILE_STORAGE_UPLOAD_MAX_SIZE
),
headers: {
"User-Agent": chromeUserAgent,
},
headers,
timeout: 10000,
...init,
...initWithoutHeaders,
});
if (!res.ok) {