feat: add DIRECT_RESPONSE_ATTACHMENT, close #28

This commit is contained in:
liqingwei 2021-05-09 00:36:48 +08:00
parent 483b213f80
commit ba1987c7e4
7 changed files with 73 additions and 38 deletions

View File

@ -154,18 +154,19 @@ 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 |
| STORE_BUCKET | Bucket | | | true |
| STORE_END_POINT | Host name or an IP address. | | | |
| STORE_REGION | region | us-east-1 | | |
| 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 | | |
| Name | Description | Default | Optional | Required |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------- | -------- |
| PASSWORD | Password to login to the app | | | true |
| STORE_ACCESS_KEY | AccessKey | | | true |
| STORE_SECRET_KEY | SecretKey | | | true |
| STORE_BUCKET | Bucket | | | true |
| STORE_END_POINT | Host name or an IP address. | | | |
| STORE_REGION | region | us-east-1 | | |
| 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. [#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

View File

@ -60,6 +60,8 @@ export abstract class StoreProvider {
): Promise<{
content?: string
meta?: { [key: string]: string }
contentType?: string
buffer?: Buffer
}>
/**

View File

@ -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(

View File

@ -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)))
})
}

View File

@ -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 {

View File

@ -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

View File

@ -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,21 +9,37 @@ 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
)
res.setHeader(
'Cache-Control',
`public, max-age=${expires}, s-maxage=${expires}, stale-while-revalidate=${expires}`
)
if (signUrl) {
res.redirect(signUrl)
return
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')