mirror of
https://github.com/QingWei-Li/notea.git
synced 2024-12-04 20:32:36 +03:00
feat: add DIRECT_RESPONSE_ATTACHMENT, close #28
This commit is contained in:
parent
483b213f80
commit
ba1987c7e4
@ -155,7 +155,7 @@ Contribution examples are welcome.
|
||||
## Environment variables
|
||||
|
||||
| Name | Description | Default | Optional | Required |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -------- | -------- |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------- | -------- |
|
||||
| PASSWORD | Password to login to the app | | | true |
|
||||
| STORE_ACCESS_KEY | AccessKey | | | true |
|
||||
| STORE_SECRET_KEY | SecretKey | | | true |
|
||||
@ -165,7 +165,8 @@ Contribution examples are welcome.
|
||||
| STORE_FORCE_PATH_STYLE | Whether to force path style URLs for S3 objects | false | | |
|
||||
| COOKIE_SECURE | Only works under https: scheme **If the website is not https, you may not be able to log in, and you need to set it to false** | true | | |
|
||||
| BASE_URL | The domain of the website, used for SEO | | | |
|
||||
| DISABLE_PASSWORD | Disable password protection. This means that you need to implement authentication on the server yourself, but the route `/share/:id` needs to be accessible anonymously, if you need share page. | false | | |
|
||||
| DISABLE_PASSWORD | Disable password protection. This means that you need to implement authentication on the server yourself, but the route `/share/:id` needs to be accessible anonymously, if you need share page. [#31](https://github.com/QingWei-Li/notea/issues/31) | false | | |
|
||||
| DIRECT_RESPONSE_ATTACHMENT | By default, requesting attachment links will redirect to S3 URL, Set to true to directly output attachments from the notea services. [#28](https://github.com/QingWei-Li/notea/issues/28) | false | | |
|
||||
|
||||
## Development
|
||||
|
||||
|
@ -60,6 +60,8 @@ export abstract class StoreProvider {
|
||||
): Promise<{
|
||||
content?: string
|
||||
meta?: { [key: string]: string }
|
||||
contentType?: string
|
||||
buffer?: Buffer
|
||||
}>
|
||||
|
||||
/**
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
S3Client,
|
||||
} from '@aws-sdk/client-s3'
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
||||
import { streamToString } from '../utils'
|
||||
import { streamToBuffer } from '../utils'
|
||||
import { Readable } from 'stream'
|
||||
import { isEmpty, toNumber } from 'lodash'
|
||||
import { Client as MinioClient } from 'minio'
|
||||
@ -104,7 +104,7 @@ export class StoreS3 extends StoreProvider {
|
||||
Key: this.getPath(path),
|
||||
})
|
||||
)
|
||||
content = await streamToString(result.Body as Readable)
|
||||
content = await streamToBuffer(result.Body as Readable)
|
||||
} catch (err) {
|
||||
if (!isNoSuchKey(err)) {
|
||||
throw err
|
||||
@ -134,6 +134,7 @@ export class StoreS3 extends StoreProvider {
|
||||
async getObjectAndMeta(path: string, isCompressed = false) {
|
||||
let content
|
||||
let meta
|
||||
let contentType
|
||||
|
||||
try {
|
||||
const result = await this.client.send(
|
||||
@ -142,15 +143,21 @@ export class StoreS3 extends StoreProvider {
|
||||
Key: this.getPath(path),
|
||||
})
|
||||
)
|
||||
content = await streamToString(result.Body as Readable)
|
||||
content = await streamToBuffer(result.Body as Readable)
|
||||
meta = result.Metadata
|
||||
contentType = result.ContentType
|
||||
} catch (err) {
|
||||
if (!isNoSuchKey(err)) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return { content: toStr(content, isCompressed), meta }
|
||||
return {
|
||||
content: toStr(content, isCompressed),
|
||||
meta,
|
||||
contentType,
|
||||
buffer: content,
|
||||
}
|
||||
}
|
||||
|
||||
async putObject(
|
||||
|
@ -2,11 +2,11 @@ import { Readable } from 'stream'
|
||||
|
||||
// Apparently the stream parameter should be of type Readable|ReadableStream|Blob
|
||||
// The latter 2 don't seem to exist anywhere.
|
||||
export async function streamToString(stream: Readable): Promise<string> {
|
||||
export async function streamToBuffer(stream: Readable): Promise<Buffer> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const chunks: Uint8Array[] = []
|
||||
stream.on('data', (chunk) => chunks.push(chunk))
|
||||
stream.on('error', reject)
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)))
|
||||
})
|
||||
}
|
||||
|
@ -1,5 +1,19 @@
|
||||
type AllowedEnvs =
|
||||
| 'PASSWORD'
|
||||
| 'STORE_ACCESS_KEY'
|
||||
| 'STORE_SECRET_KEY'
|
||||
| 'STORE_BUCKET'
|
||||
| 'STORE_END_POINT'
|
||||
| 'STORE_REGION'
|
||||
| 'STORE_FORCE_PATH_STYLE'
|
||||
| 'COOKIE_SECURE'
|
||||
| 'BASE_URL'
|
||||
| 'DISABLE_PASSWORD'
|
||||
| 'DIRECT_RESPONSE_ATTACHMENT'
|
||||
| 'IS_DEMO'
|
||||
|
||||
export function getEnv<T>(
|
||||
env: string,
|
||||
env: AllowedEnvs,
|
||||
defaultValue?: any,
|
||||
required = false
|
||||
): T {
|
||||
|
@ -17,7 +17,7 @@ export function toStr(
|
||||
if (!bufferOrString) return
|
||||
|
||||
const str = Buffer.isBuffer(bufferOrString)
|
||||
? bufferOrString.toString()
|
||||
? bufferOrString.toString('utf-8')
|
||||
: bufferOrString
|
||||
|
||||
return deCompressed ? strDecompress(str) : str
|
||||
|
@ -1,12 +1,7 @@
|
||||
import { api } from 'libs/server/connect'
|
||||
import { useStore } from 'libs/server/middlewares/store'
|
||||
import { getPathFileByName } from 'libs/server/note-path'
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
}
|
||||
import { getEnv } from 'libs/shared/env'
|
||||
|
||||
// On aliyun `X-Amz-Expires` must be less than 604800 seconds
|
||||
const expires = 604800 - 1
|
||||
@ -14,22 +9,38 @@ const expires = 604800 - 1
|
||||
export default api()
|
||||
.use(useStore)
|
||||
.get(async (req, res) => {
|
||||
if (req.query.file) {
|
||||
const signUrl = await req.state.store.getSignUrl(
|
||||
getPathFileByName((req.query.file as string[]).join('/')),
|
||||
expires
|
||||
)
|
||||
if (!req.query.file) {
|
||||
res.redirect('/404')
|
||||
return
|
||||
}
|
||||
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
`public, max-age=${expires}, s-maxage=${expires}, stale-while-revalidate=${expires}`
|
||||
)
|
||||
|
||||
const objectPath = getPathFileByName((req.query.file as string[]).join('/'))
|
||||
const directed = getEnv<boolean>('DIRECT_RESPONSE_ATTACHMENT', false)
|
||||
|
||||
if (directed) {
|
||||
const { buffer, contentType } = await req.state.store.getObjectAndMeta(
|
||||
objectPath
|
||||
)
|
||||
|
||||
if (contentType) {
|
||||
res.setHeader('Content-Type', contentType)
|
||||
}
|
||||
|
||||
res.send(buffer)
|
||||
return
|
||||
}
|
||||
|
||||
const signUrl = await req.state.store.getSignUrl(objectPath, expires)
|
||||
|
||||
if (signUrl) {
|
||||
res.redirect(signUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect('/404')
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user