file

package
v1.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 4, 2026 License: Apache-2.0 Imports: 49 Imported by: 0

Documentation

Index

Constants

View Source
const MaxFileSize int64 = 100 * 1024 * 1024

MaxFileSize 最大文件大小(100MB)

View Source
const StickerMaxDimension = 512

StickerMaxDimension 自定义贴纸解码后单边像素上限(512×512)。文件大小上限并不 约束解码维度:一张高压缩比的小文件可解出极大位图(decompression bomb),把内联 渲染端的内存撑爆——而贴纸会发送给会话对方,等于跨用户 DoS。上传时用 image.DecodeConfig 只读图像头拿 W×H(不解整图)并卡死此上限。

View Source
const StickerMaxFileSize int64 = 1 * 1024 * 1024

StickerMaxFileSize 自定义贴纸单文件上限(1MB)。贴纸是高频内联渲染的小图, 收紧到 1MB(对标业界:Discord 贴纸 512KB、微信 ~1MB),避免大图占用与卡顿。

Variables

This section is empty.

Functions

func BuildContentDisposition

func BuildContentDisposition(filename string) string

func IsAllowedExtension

func IsAllowedExtension(ext string) bool

IsAllowedExtension 检查文件扩展名是否允许上传

func IsBlockedExtension

func IsBlockedExtension(ext string) bool

IsBlockedExtension 检查文件扩展名是否被禁止

func ValidateMagicNumber

func ValidateMagicNumber(ext string, header []byte) bool

ValidateMagicNumber 验证文件内容的魔数是否与扩展名匹配 返回 true 表示验证通过(内容与扩展名一致或该扩展名无需验证)

Types

type File

type File struct {
	log.Log
	// contains filtered or unexported fields
}

File 文件操作

func New

func New(ctx *config.Context) *File

New New

func (*File) Route

func (f *File) Route(r *wkhttp.WKHttp)

Route 路由

type IService

type IService interface {
	IUploadService
	DownloadAndMakeCompose(uploadPath string, downloadURLs []string) (map[string]interface{}, error)
	DownloadImage(url string, ctx context.Context) (io.ReadCloser, error)
	// PresignedPutURL signs a direct-to-storage PUT URL. `fileSize` is the
	// exact byte length the client commits to upload; the storage backend
	// signs `Content-Length: <fileSize>` (or its OSS equivalent) into the
	// canonical headers, so any deviation at PUT time is rejected by the
	// gateway with a SignatureDoesNotMatch / SizeMismatch response. This
	// is what lets the presigned-PUT path enforce the same MaxFileSize cap
	// the multipart `uploadFile` handler enforces server-side; without it
	// any authenticated caller could upload arbitrary bytes under an
	// allowed extension and bypass the size gate entirely.
	PresignedPutURL(objectPath string, contentType string, contentDisposition string, fileSize int64, expires time.Duration) (uploadURL string, downloadURL string, err error)
	PresignedGetURL(objectPath string, filename string, disposition string, expires time.Duration) (string, error)
}

IService IService

func NewService

func NewService(ctx *config.Context) IService

NewService NewService

type IUploadService

type IUploadService interface {
	UploadFile(filePath string, contentType string, contentDisposition string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error)
	// 获取下载地址
	DownloadURL(path string, filename string) (string, error)
	// 直接读取文件内容(用于 MinIO 等非 public bucket)
	GetFile(path string) (io.ReadCloser, string, error)
}

type PresignedGetter

type PresignedGetter interface {
	PresignedGetURL(objectPath string, filename string, disposition string, expires time.Duration) (string, error)
}

type PresignedPutter

type PresignedPutter interface {
	PresignedPutURL(objectPath string, contentType string, contentDisposition string, fileSize int64, expires time.Duration) (uploadURL string, downloadURL string, err error)
}

type SeaweedFS

type SeaweedFS struct {
	log.Log
	// contains filtered or unexported fields
}

func NewSeaweedFS

func NewSeaweedFS(ctx *config.Context) *SeaweedFS

func (*SeaweedFS) DownloadURL

func (s *SeaweedFS) DownloadURL(path string, filename string) (string, error)

func (*SeaweedFS) GetFile

func (s *SeaweedFS) GetFile(path string) (io.ReadCloser, string, error)

func (*SeaweedFS) PresignedGetURL

func (s *SeaweedFS) PresignedGetURL(objectPath string, filename string, disposition string, expires time.Duration) (string, error)

PresignedGetURL is intentionally not implemented for SeaweedFS.

Public SeaweedFS reads are unsigned — the volume URL itself is the download URL. Callers that want a signed download against SeaweedFS should use the regular DownloadURL path; the IService surface keeps the hook here so the type assertion in service.go succeeds and we fail loudly only when a signed URL is genuinely required.

func (*SeaweedFS) PresignedPutURL

func (s *SeaweedFS) PresignedPutURL(objectPath string, contentType string, contentDisposition string, fileSize int64, expires time.Duration) (string, string, error)

PresignedPutURL is intentionally not implemented for SeaweedFS.

The standard SeaweedFS Filer/Volume HTTP API does not expose a presigned upload primitive: uploads go through the multipart-form helper used by UploadFile above, and the volume server authenticates by IP / network segmentation rather than by signed URL. Returning a clear error keeps the IService surface uniform; deployments needing browser-direct upload should sit a presign-capable proxy (or the SeaweedFS S3 gateway, when configured) in front of SeaweedFS, or fall back to server-side upload via UploadFile.

func (*SeaweedFS) UploadFile

func (s *SeaweedFS) UploadFile(filePath string, contentType string, contentDisposition string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error)

UploadFile 上传文件

type Service

type Service struct {
	log.Log
	// contains filtered or unexported fields
}

Service Service

func (*Service) DownloadAndMakeCompose

func (s *Service) DownloadAndMakeCompose(uploadPath string, downloadURLs []string) (map[string]interface{}, error)

DownloadAndMakeCompose 下载并组合图片

func (*Service) DownloadImage

func (s *Service) DownloadImage(url string, ctx context.Context) (io.ReadCloser, error)

func (*Service) DownloadURL

func (s *Service) DownloadURL(path string, filename string) (string, error)

func (*Service) GetFile

func (s *Service) GetFile(path string) (io.ReadCloser, string, error)

func (*Service) MakeCompose

func (s *Service) MakeCompose(srcImgFiles []io.ReadCloser) (image.Image, error)

MakeCompose 组合图片

func (*Service) PresignedGetURL

func (s *Service) PresignedGetURL(objectPath string, filename string, disposition string, expires time.Duration) (string, error)

func (*Service) PresignedPutURL

func (s *Service) PresignedPutURL(objectPath string, contentType string, contentDisposition string, fileSize int64, expires time.Duration) (string, string, error)

func (*Service) UploadFile

func (s *Service) UploadFile(filePath string, contentType string, contentDisposition string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error)

type ServiceCOS

type ServiceCOS struct {
	log.Log
	// contains filtered or unexported fields
}

ServiceCOS 腾讯云COS文件上传(通过S3兼容协议)

func NewServiceCOS

func NewServiceCOS(ctx *config.Context) *ServiceCOS

NewServiceCOS NewServiceCOS

func (*ServiceCOS) DownloadURL

func (sc *ServiceCOS) DownloadURL(ph string, filename string) (string, error)

DownloadURL builds a browser-facing object URL that respects the addressing style chosen by `publicEndpoint`. The result MUST land on the same host (and path shape) as the presigned GET URL emitted by `PresignedGetURL`, otherwise an upload-then-download flow returns 404 even when the PUT succeeded.

Hotfix history: this function previously concatenated `BucketURL` with the object key directly, which silently dropped the bucket segment for path-style CDN deployments (BucketURL=`https://cdn.example.com`):

  • PresignedPutURL → `https://cdn.example.com/<bucket>/<prefix>/<key>` ✅ (signed by `newPublicClient` with `BucketLookupPath`)
  • DownloadURL → `https://cdn.example.com/<prefix>/<key>` ❌ (missing bucket segment → next browser GET = 404)

`PresignedPutURL` calls `DownloadURL` to populate the `downloadUrl` field returned by `/v1/file/upload-credentials`, so the mismatch shipped to every browser client. This is the YUJ-848 follow-up to the YUJ-846 path-style fix in PR#56 — the sibling `PresignedPutURL` / `PresignedGetURL` paths got `BucketLookupPath` in PR#56, and this function now matches.

func (*ServiceCOS) GetFile

func (sc *ServiceCOS) GetFile(ph string) (io.ReadCloser, string, error)

DownloadURL 获取COS文件下载地址

func (*ServiceCOS) PresignedGetURL

func (sc *ServiceCOS) PresignedGetURL(objectPath string, filename string, disposition string, expires time.Duration) (string, error)

PresignedGetURL 生成预签名 GET URL,带 response-content-disposition 用于下载。

Client selection follows the same logic as PresignedPutURL: bucket-subdomain / empty BucketURL → sign via `newPublicClient`; CDN alias BucketURL → sign via `getClient` (canonical COS endpoint) because the CDN domain has no route for path-style `/<bucket>/<key>`. (YUJ-877 / GH#57 fix — see PresignedPutURL for the full rationale.)

func (*ServiceCOS) PresignedPutURL

func (sc *ServiceCOS) PresignedPutURL(objectPath string, contentType string, contentDisposition string, fileSize int64, expires time.Duration) (uploadURL string, downloadURL string, err error)

PresignedPutURL 生成预签名 PUT URL,用于客户端直传 COS。

Client selection depends on the BucketURL shape:

  • Bucket-subdomain BucketURL (e.g. `https://<bucket>.cos.example.com`) or empty BucketURL: sign against the browser-facing endpoint via `newPublicClient`. SigV4 covers `host`, so the browser hits the same host the signature covers.

  • CDN alias BucketURL (e.g. `https://cdn.deepminer.com.cn`): sign against the canonical COS endpoint via `getClient` (virtual-hosted `<bucket>.cos.<region>.myqcloud.com`). CDN domains are bucket aliases — the CDN origin routes to the bucket implicitly, so the SDK's path-style `/<bucket>/<key>` URL shape has no corresponding route on the CDN. Signing against the real COS endpoint means the browser uploads/downloads directly to COS, bypassing the CDN. `publicURL` uses the CDN for non-presigned download URLs only. (YUJ-877 / GH#57 fix)

fileSize is signed into the canonical-headers section as `Content-Length`. The browser MUST echo the same value (browsers compute it automatically from the request body length); any mismatch is rejected by COS as 403 SignatureDoesNotMatch — same enforcement model as the MinIO backend, see service_minio.go for the rationale.

func (*ServiceCOS) UploadFile

func (sc *ServiceCOS) UploadFile(filePath string, contentType string, contentDisposition string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error)

UploadFile 上传文件到腾讯云COS

type ServiceMinio

type ServiceMinio struct {
	log.Log
	// contains filtered or unexported fields
}

ServiceMinio 文件上传

func NewServiceMinio

func NewServiceMinio(ctx *config.Context) *ServiceMinio

NewServiceMinio NewServiceMinio

func (*ServiceMinio) DownloadURL

func (sm *ServiceMinio) DownloadURL(ph string, filename string) (string, error)

func (*ServiceMinio) GetFile

func (sm *ServiceMinio) GetFile(ph string) (io.ReadCloser, string, error)

func (*ServiceMinio) PresignedGetURL

func (sm *ServiceMinio) PresignedGetURL(objectPath string, filename string, disposition string, expires time.Duration) (string, error)

PresignedGetURL generates a presigned GET URL with a Content-Disposition override so the browser saves the file under the correct user-facing filename. The URL is signed against the browser-facing endpoint (`publicEndpoint`); no post-sign host rewriting is performed. MinIO 默认 bucket 为公共读,但鉴权模式下也通过此方法签发。

func (*ServiceMinio) PresignedPutURL

func (sm *ServiceMinio) PresignedPutURL(objectPath string, contentType string, contentDisposition string, fileSize int64, expires time.Duration) (uploadURL string, downloadURL string, err error)

PresignedPutURL generates a presigned PUT URL the browser can use to upload directly to MinIO, plus the matching anonymous GET URL for the resulting object. The target bucket is bootstrapped on first use via `ensureBucket` so a presigned PUT against a fresh deployment never lands on a NoSuchBucket response.

The returned URL is signed against the *browser-facing* endpoint (`publicEndpoint`), not the server-internal one. SigV4 includes `host` in the signed headers, so any post-sign host change would invalidate the signature; signing with the public host up front is the only way for the resulting URL to be valid as-is from a browser. Bucket bootstrap still runs against the internal client because it needs network reachability, not signature validity for the browser.

`fileSize` is signed into the canonical-headers section as `Content-Length`. The browser MUST echo the same Content-Length on its PUT (browsers compute this automatically from the request body length), and the storage gateway rejects any mismatch with SignatureDoesNotMatch. This is the server-side enforcement of the upload size cap on the presigned path: a caller cannot exceed the signed budget without invalidating the URL — closes the size-bypass security gap that the multipart `uploadFile` handler closes via `http.MaxBytesReader` + `MaxFileSize` on the request body. Pass a non-positive `fileSize` and the function returns an error rather than silently producing an unbounded URL.

func (*ServiceMinio) UploadFile

func (sm *ServiceMinio) UploadFile(filePath string, contentType string, contentDisposition string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error)

UploadFile 上传文件

type ServiceOSS

type ServiceOSS struct {
	log.Log
	// contains filtered or unexported fields
}

func NewServiceOSS

func NewServiceOSS(ctx *config.Context) *ServiceOSS

NewServiceOSS NewServiceOSS

func (*ServiceOSS) DownloadURL

func (s *ServiceOSS) DownloadURL(path string, filename string) (string, error)

DownloadURL returns the public anonymous-GET URL for an object stored in the configured OSS bucket. The input `path` is the same shape that the file API at `modules/file/api.go` produces — `<fileType>/<...>` (e.g. `chat/2025/x.png`) — possibly with a leading slash.

The path is routed through `normalizeOSSObjectKey` before being joined with `BucketURL` so that the resulting URL points at the same OSS object the upload paths actually wrote: `UploadFile` and `PresignedPutURL` both strip a leading `<BucketName>/` segment to avoid double-bucketing the stored key. Without that normalization step here, the asymmetric case where the deployer's bucket name happens to equal a `fileType` prefix (e.g. `OSS.BucketName == "chat"`, `path == "chat/2025/x.png"` → object stored as `2025/x.png`) would emit a URL like `<BucketURL>/chat/2025/x.png` and 404. PR#50 R5 codex finding 2.4 closed this asymmetry on the upload side; lml2468 surfaced the surviving download-side mismatch in PR#50 R6.

The `filename` argument is preserved for API symmetry with other backends but is not used for OSS V1 download URLs (operators wanting per-request filename overrides should call `PresignedGetURL` instead, which embeds `response-content-disposition` in the signed URL).

func (*ServiceOSS) GetFile

func (s *ServiceOSS) GetFile(path string) (io.ReadCloser, string, error)

func (*ServiceOSS) PresignedGetURL

func (s *ServiceOSS) PresignedGetURL(objectPath string, filename string, disposition string, expires time.Duration) (string, error)

PresignedGetURL signs an OSS GET URL with a `response-content-disposition` override so the browser saves the file under the user-facing filename.

func (*ServiceOSS) PresignedPutURL

func (s *ServiceOSS) PresignedPutURL(objectPath string, contentType string, contentDisposition string, fileSize int64, expires time.Duration) (uploadURL string, downloadURL string, err error)

PresignedPutURL signs an OSS PUT URL the browser can use directly. Aliyun OSS does NOT accept a separate Content-Disposition signature on PUT the way S3 does — disposition has to be embedded as object metadata at upload time. We therefore include it in the signed headers so the client echoes the same value, and the OSS gateway records it on the resulting object.

Caveat — Content-Length on OSS V1: the OSS V1 canonical-string algorithm does NOT cover Content-Length. Although we pass `oss.ContentLength` into SignURL the resulting signature is computed over Date / Content-Type / Canonicalized OSSHeaders / Resource — the size is advisory only and the OSS gateway accepts a PUT of any byte length under the signed URL. This is the OSS-side deviation from the SigV4 backends (MinIO/COS), where Content-Length IS folded into `X-Amz-SignedHeaders` and a wrong or missing value at PUT time returns 403 SignatureDoesNotMatch. To enforce a hard size budget on OSS, operators must rely on bucket / account-level policies (e.g. lifecycle quotas, RAM/STS policies that cap object size) — this server's signed URL alone cannot. We still require a positive `fileSize` so the request is well-formed and the API contract (`maxFileSize` echo) stays consistent across backends.

Caveat — Content-Disposition on OSS V1: same root cause. The OSS V1 canonical-string algorithm also does NOT include Content-Disposition, so a deviating value at PUT time does NOT produce SignatureDoesNotMatch the way it does on MinIO/COS. The browser-supplied Content-Disposition (or absence of one) is silently persisted. Operators who need strict disposition or size enforcement should migrate to a SigV4 backend (MinIO/COS), or run a post-upload validator. See `getUploadCredentials` docstring for the full per-header deviation matrix.

Roadmap — OSS V4 signing covers Content-Length canonically; switching the SDK call to `oss.WithSignVersion(oss.SignVersionV4)` would close this gap. Tracked separately so this PR can ship the customer-facing SigV4 fix without dragging the OSS SDK uplift along.

func (*ServiceOSS) UploadFile

func (s *ServiceOSS) UploadFile(filePath string, contentType string, contentDisposition string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error)

UploadFile 上传文件

type ServiceQiniu

type ServiceQiniu struct {
	log.Log
	// contains filtered or unexported fields
}

func NewServiceQiniu

func NewServiceQiniu(ctx *config.Context) *ServiceQiniu

NewServiceQiniu NewServiceQiniu

func (*ServiceQiniu) DownloadURL

func (s *ServiceQiniu) DownloadURL(path string, filename string) (string, error)

func (*ServiceQiniu) GetFile

func (s *ServiceQiniu) GetFile(path string) (io.ReadCloser, string, error)

func (*ServiceQiniu) PresignedGetURL

func (s *ServiceQiniu) PresignedGetURL(objectPath string, filename string, disposition string, expires time.Duration) (string, error)

PresignedGetURL signs a private-bucket download URL for Qiniu, with the `attname` query parameter set so the browser saves under the user-facing filename.

func (*ServiceQiniu) PresignedPutURL

func (s *ServiceQiniu) PresignedPutURL(objectPath string, contentType string, contentDisposition string, fileSize int64, expires time.Duration) (string, string, error)

PresignedPutURL is intentionally not implemented for Qiniu.

Qiniu's direct-upload contract is fundamentally different from the S3 presigned-PUT model that the rest of the IService surface assumes: a browser uploads to a fixed upload host (e.g. `up.qiniup.com`) using a multipart form whose `token` field carries an `UploadToken` derived from a `PutPolicy`. There is no single signed URL the browser can `PUT` to.

Surfacing that asymmetry as a clear error here keeps the IService signatures uniform across backends; deployments that need browser-direct upload to Qiniu should instead use the standard server-side UploadFile path or migrate to the COS / OSS / MinIO backends. The `configs/octo-server.yaml` support matrix documents this fallback.

func (*ServiceQiniu) UploadFile

func (s *ServiceQiniu) UploadFile(filePath string, contentType string, contentDisposition string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error)

UploadFile 上传文件

type ServiceS3 added in v1.5.0

type ServiceS3 struct {
	log.Log
	// contains filtered or unexported fields
}

ServiceS3 implements a single-bucket S3 / S3-compatible storage backend.

Targets AWS S3 and any vendor that speaks the S3 protocol with SigV4 (Cloudflare R2, Backblaze B2, Wasabi, MinIO with a custom endpoint, ...). Configuration comes from cfg.S3 — see octo-lib config.S3Config for the field semantics and Viper bindings.

Design contrast with ServiceMinio / ServiceCOS:

  • ServiceMinio routes by path prefix into N buckets and pins Region: us-east-1. ServiceS3 uses a single bucket from cfg.S3.Bucket and the explicit cfg.S3.Region; the upload type segment ("chat/", "moment/", ...) becomes part of the object key inside that one bucket.
  • ServiceCOS bakes in cos.<region>.myqcloud.com endpoint templating plus a CDN-alias-vs-canonical client switch for path-style CDN fronts. ServiceS3 takes the endpoint verbatim from cfg.S3.Endpoint and supports UsePathStyle as an explicit operator opt-in.

Signing model: presigned PUT/GET URLs always sign against cfg.S3.Endpoint. cfg.S3.DownloadURL is the browser-facing prefix for UNSIGNED URLs only (preview redirects, upload response path) — it never participates in SigV4. This deliberate separation removes the previous host-shape detection on BucketURL and aligns with the octo-lib S3Config contract (see config.S3Config.DownloadURL godoc).

func NewServiceS3 added in v1.5.0

func NewServiceS3(ctx *config.Context) *ServiceS3

NewServiceS3 constructs a ServiceS3 from the active config. Required fields (Endpoint, Region, Bucket, AccessKeyID, SecretAccessKey) are NOT validated here — the constructor returns a usable struct even when fields are missing so the process can start (matching the existing ServiceMinio / ServiceCOS constructors), and per-request errors surface the missing configuration at the first call site. Operators should treat "S3 配置缺失" log lines as a startup failure.

func (*ServiceS3) DownloadURL added in v1.5.0

func (s *ServiceS3) DownloadURL(ph string, filename string) (string, error)

DownloadURL returns a browser-facing URL for the object. This is the *unsigned* URL — useful only when the bucket allows anonymous GET, or when fronted by a CDN that handles auth out-of-band. For private buckets (the AWS default with Block Public Access on), callers should use PresignedGetURL instead; modules/file/api.go's getFile handler redirects through DownloadURL but the public API also exposes /v1/file/download/url that goes through PresignedGetURL.

func (*ServiceS3) GetFile added in v1.5.0

func (s *ServiceS3) GetFile(ph string) (io.ReadCloser, string, error)

GetFile fetches the object body for server-side handlers that need to proxy the file (e.g. legacy /v1/file/preview path). Buckets with Block Public Access turned on are still readable here because the SDK signs each request.

func (*ServiceS3) PresignedGetURL added in v1.5.0

func (s *ServiceS3) PresignedGetURL(objectPath string, filename string, disposition string, expires time.Duration) (string, error)

PresignedGetURL signs a GET URL with a Content-Disposition override so the browser downloads with the operator-supplied filename instead of the raw object key. Signing host is always cfg.S3.Endpoint (see file-level doc).

func (*ServiceS3) PresignedPutURL added in v1.5.0

func (s *ServiceS3) PresignedPutURL(objectPath string, contentType string, contentDisposition string, fileSize int64, expires time.Duration) (uploadURL string, downloadURL string, err error)

PresignedPutURL signs a direct-to-storage PUT URL. fileSize is signed into the canonical-headers section as Content-Length; any deviation the browser sends is rejected by the gateway with 403 SignatureDoesNotMatch. This is the server-side enforcement of MaxFileSize on the presigned path — same model as ServiceMinio / ServiceCOS, see modules/file/api.go getUploadCredentials for the full signed-header contract returned to clients.

Signing host is always cfg.S3.Endpoint; cfg.S3.DownloadURL has no influence on the signed URL. The returned downloadURL (unsigned) is constructed via publicURL and may carry the DownloadURL prefix.

func (*ServiceS3) UploadFile added in v1.5.0

func (s *ServiceS3) UploadFile(filePath string, contentType string, contentDisposition string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error)

UploadFile streams a single object into the configured S3 bucket. The full filePath (e.g. "chat/2026/foo.jpg") becomes the object key after the optional cfg.S3.Prefix is prepended; the single-bucket model means the file-type segment lives in the key, not the bucket.

type Type

type Type string

Type 文件类型

const (
	// TypeChat 聊天文件
	TypeChat Type = "chat"
	// TypeMoment 动态文件
	TypeMoment Type = "moment"
	// TypeMomentCover 动态封面
	TypeMomentCover Type = "momentcover"
	// TypeSticker 表情
	TypeSticker Type = "sticker"
	// TypeReport 举报
	TypeReport Type = "report"
	// TypeCommon 通用
	TypeCommon Type = "common"
	// TypeChatBg 聊天背景
	TypeChatBg Type = "chatbg"
	// TypeDownload 下载文件目录
	TypeDownload = "download"
	// TypeWorkplaceBanner
	TypeWorkplaceBanner Type = "workplacebanner"
	// TypeWorkplaceAppIcon
	TypeWorkplaceAppIcon Type = "workplaceappicon"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL