Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
31997936
Commit
31997936
authored
Oct 08, 2023
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: move resource public api
parent
287f1beb
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
169 additions
and
140 deletions
+169
-140
resource.go
api/resource/resource.go
+165
-0
resource.go
api/v1/resource.go
+0
-139
v1.go
api/v1/v1.go
+4
-1
No files found.
api/resource/resource.go
0 → 100644
View file @
31997936
package
resource
import
(
"bytes"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/disintegration/imaging"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"go.uber.org/zap"
"github.com/usememos/memos/common/log"
"github.com/usememos/memos/common/util"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
)
const
(
// The key name used to store user id in the context
// user id is extracted from the jwt token subject field.
userIDContextKey
=
"user-id"
// thumbnailImagePath is the directory to store image thumbnails.
thumbnailImagePath
=
".thumbnail_cache"
)
type
Service
struct
{
Profile
*
profile
.
Profile
Store
*
store
.
Store
}
func
NewService
(
profile
*
profile
.
Profile
,
store
*
store
.
Store
)
*
Service
{
return
&
Service
{
Profile
:
profile
,
Store
:
store
,
}
}
func
(
s
*
Service
)
RegisterResourcePublicRoutes
(
g
*
echo
.
Group
)
{
g
.
GET
(
"/r/:resourceId"
,
s
.
streamResource
)
g
.
GET
(
"/r/:resourceId/*"
,
s
.
streamResource
)
}
func
(
s
*
Service
)
streamResource
(
c
echo
.
Context
)
error
{
ctx
:=
c
.
Request
()
.
Context
()
resourceID
,
err
:=
util
.
ConvertStringToInt32
(
c
.
Param
(
"resourceId"
))
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
fmt
.
Sprintf
(
"ID is not a number: %s"
,
c
.
Param
(
"resourceId"
)))
.
SetInternal
(
err
)
}
resource
,
err
:=
s
.
Store
.
GetResource
(
ctx
,
&
store
.
FindResource
{
ID
:
&
resourceID
,
GetBlob
:
true
,
})
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"Failed to find resource by ID: %v"
,
resourceID
))
.
SetInternal
(
err
)
}
if
resource
==
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusNotFound
,
fmt
.
Sprintf
(
"Resource not found: %d"
,
resourceID
))
}
// Check the related memo visibility.
if
resource
.
MemoID
!=
nil
{
memo
,
err
:=
s
.
Store
.
GetMemo
(
ctx
,
&
store
.
FindMemo
{
ID
:
resource
.
MemoID
,
})
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"Failed to find memo by ID: %v"
,
resource
.
MemoID
))
.
SetInternal
(
err
)
}
if
memo
!=
nil
&&
memo
.
Visibility
!=
store
.
Public
{
userID
,
ok
:=
c
.
Get
(
userIDContextKey
)
.
(
int32
)
if
!
ok
||
(
memo
.
Visibility
==
store
.
Private
&&
userID
!=
resource
.
CreatorID
)
{
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Resource visibility not match"
)
}
}
}
blob
:=
resource
.
Blob
if
resource
.
InternalPath
!=
""
{
resourcePath
:=
resource
.
InternalPath
src
,
err
:=
os
.
Open
(
resourcePath
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"Failed to open the local resource: %s"
,
resourcePath
))
.
SetInternal
(
err
)
}
defer
src
.
Close
()
blob
,
err
=
io
.
ReadAll
(
src
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"Failed to read the local resource: %s"
,
resourcePath
))
.
SetInternal
(
err
)
}
}
if
c
.
QueryParam
(
"thumbnail"
)
==
"1"
&&
util
.
HasPrefixes
(
resource
.
Type
,
"image/png"
,
"image/jpeg"
)
{
ext
:=
filepath
.
Ext
(
resource
.
Filename
)
thumbnailPath
:=
filepath
.
Join
(
s
.
Profile
.
Data
,
thumbnailImagePath
,
fmt
.
Sprintf
(
"%d%s"
,
resource
.
ID
,
ext
))
thumbnailBlob
,
err
:=
getOrGenerateThumbnailImage
(
blob
,
thumbnailPath
)
if
err
!=
nil
{
log
.
Warn
(
fmt
.
Sprintf
(
"failed to get or generate local thumbnail with path %s"
,
thumbnailPath
),
zap
.
Error
(
err
))
}
else
{
blob
=
thumbnailBlob
}
}
c
.
Response
()
.
Writer
.
Header
()
.
Set
(
echo
.
HeaderCacheControl
,
"max-age=31536000, immutable"
)
c
.
Response
()
.
Writer
.
Header
()
.
Set
(
echo
.
HeaderContentSecurityPolicy
,
"default-src 'self'"
)
resourceType
:=
strings
.
ToLower
(
resource
.
Type
)
if
strings
.
HasPrefix
(
resourceType
,
"text"
)
{
resourceType
=
echo
.
MIMETextPlainCharsetUTF8
}
else
if
strings
.
HasPrefix
(
resourceType
,
"video"
)
||
strings
.
HasPrefix
(
resourceType
,
"audio"
)
{
http
.
ServeContent
(
c
.
Response
(),
c
.
Request
(),
resource
.
Filename
,
time
.
Unix
(
resource
.
UpdatedTs
,
0
),
bytes
.
NewReader
(
blob
))
return
nil
}
c
.
Response
()
.
Writer
.
Header
()
.
Set
(
"Content-Disposition"
,
fmt
.
Sprintf
(
`filename="%s"`
,
resource
.
Filename
))
return
c
.
Stream
(
http
.
StatusOK
,
resourceType
,
bytes
.
NewReader
(
blob
))
}
var
availableGeneratorAmount
int32
=
32
func
getOrGenerateThumbnailImage
(
srcBlob
[]
byte
,
dstPath
string
)
([]
byte
,
error
)
{
if
_
,
err
:=
os
.
Stat
(
dstPath
);
err
!=
nil
{
if
!
errors
.
Is
(
err
,
os
.
ErrNotExist
)
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to check thumbnail image stat"
)
}
if
atomic
.
LoadInt32
(
&
availableGeneratorAmount
)
<=
0
{
return
nil
,
errors
.
New
(
"not enough available generator amount"
)
}
atomic
.
AddInt32
(
&
availableGeneratorAmount
,
-
1
)
defer
func
()
{
atomic
.
AddInt32
(
&
availableGeneratorAmount
,
1
)
}()
reader
:=
bytes
.
NewReader
(
srcBlob
)
src
,
err
:=
imaging
.
Decode
(
reader
,
imaging
.
AutoOrientation
(
true
))
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to decode thumbnail image"
)
}
thumbnailImage
:=
imaging
.
Resize
(
src
,
512
,
0
,
imaging
.
Lanczos
)
dstDir
:=
path
.
Dir
(
dstPath
)
if
err
:=
os
.
MkdirAll
(
dstDir
,
os
.
ModePerm
);
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to create thumbnail dir"
)
}
if
err
:=
imaging
.
Save
(
thumbnailImage
,
dstPath
);
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to resize thumbnail image"
)
}
}
dstFile
,
err
:=
os
.
Open
(
dstPath
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to open the local resource"
)
}
defer
dstFile
.
Close
()
dstBlob
,
err
:=
io
.
ReadAll
(
dstFile
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to read the local resource"
)
}
return
dstBlob
,
nil
}
api/v1/resource.go
View file @
31997936
package
v1
import
(
"bytes"
"context"
"encoding/json"
"fmt"
...
...
@@ -9,15 +8,12 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/disintegration/imaging"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"go.uber.org/zap"
...
...
@@ -82,11 +78,6 @@ func (s *APIV1Service) registerResourceRoutes(g *echo.Group) {
g
.
DELETE
(
"/resource/:resourceId"
,
s
.
DeleteResource
)
}
func
(
s
*
APIV1Service
)
registerResourcePublicRoutes
(
g
*
echo
.
Group
)
{
g
.
GET
(
"/r/:resourceId"
,
s
.
streamResource
)
g
.
GET
(
"/r/:resourceId/*"
,
s
.
streamResource
)
}
// GetResourceList godoc
//
// @Summary Get a list of resources
...
...
@@ -362,91 +353,6 @@ func (s *APIV1Service) UpdateResource(c echo.Context) error {
return
c
.
JSON
(
http
.
StatusOK
,
convertResourceFromStore
(
resource
))
}
// streamResource godoc
//
// @Summary Stream a resource
// @Description *Swagger UI may have problems displaying other file types than images
// @Tags resource
// @Produce octet-stream
// @Param resourceId path int true "Resource ID"
// @Param thumbnail query int false "Thumbnail"
// @Success 200 {object} nil "Requested resource"
// @Failure 400 {object} nil "ID is not a number: %s | Failed to get resource visibility"
// @Failure 401 {object} nil "Resource visibility not match"
// @Failure 404 {object} nil "Resource not found: %d"
// @Failure 500 {object} nil "Failed to find resource by ID: %v | Failed to open the local resource: %s | Failed to read the local resource: %s"
// @Router /o/r/{resourceId} [GET]
func
(
s
*
APIV1Service
)
streamResource
(
c
echo
.
Context
)
error
{
ctx
:=
c
.
Request
()
.
Context
()
resourceID
,
err
:=
util
.
ConvertStringToInt32
(
c
.
Param
(
"resourceId"
))
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
fmt
.
Sprintf
(
"ID is not a number: %s"
,
c
.
Param
(
"resourceId"
)))
.
SetInternal
(
err
)
}
resource
,
err
:=
s
.
Store
.
GetResource
(
ctx
,
&
store
.
FindResource
{
ID
:
&
resourceID
,
GetBlob
:
true
,
})
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"Failed to find resource by ID: %v"
,
resourceID
))
.
SetInternal
(
err
)
}
if
resource
==
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusNotFound
,
fmt
.
Sprintf
(
"Resource not found: %d"
,
resourceID
))
}
// Check the related memo visibility.
if
resource
.
MemoID
!=
nil
{
memo
,
err
:=
s
.
Store
.
GetMemo
(
ctx
,
&
store
.
FindMemo
{
ID
:
resource
.
MemoID
,
})
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"Failed to find memo by ID: %v"
,
resource
.
MemoID
))
.
SetInternal
(
err
)
}
if
memo
!=
nil
&&
memo
.
Visibility
!=
store
.
Public
{
userID
,
ok
:=
c
.
Get
(
userIDContextKey
)
.
(
int32
)
if
!
ok
||
(
memo
.
Visibility
==
store
.
Private
&&
userID
!=
resource
.
CreatorID
)
{
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Resource visibility not match"
)
}
}
}
blob
:=
resource
.
Blob
if
resource
.
InternalPath
!=
""
{
resourcePath
:=
resource
.
InternalPath
src
,
err
:=
os
.
Open
(
resourcePath
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"Failed to open the local resource: %s"
,
resourcePath
))
.
SetInternal
(
err
)
}
defer
src
.
Close
()
blob
,
err
=
io
.
ReadAll
(
src
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
fmt
.
Sprintf
(
"Failed to read the local resource: %s"
,
resourcePath
))
.
SetInternal
(
err
)
}
}
if
c
.
QueryParam
(
"thumbnail"
)
==
"1"
&&
util
.
HasPrefixes
(
resource
.
Type
,
"image/png"
,
"image/jpeg"
)
{
ext
:=
filepath
.
Ext
(
resource
.
Filename
)
thumbnailPath
:=
filepath
.
Join
(
s
.
Profile
.
Data
,
thumbnailImagePath
,
fmt
.
Sprintf
(
"%d%s"
,
resource
.
ID
,
ext
))
thumbnailBlob
,
err
:=
getOrGenerateThumbnailImage
(
blob
,
thumbnailPath
)
if
err
!=
nil
{
log
.
Warn
(
fmt
.
Sprintf
(
"failed to get or generate local thumbnail with path %s"
,
thumbnailPath
),
zap
.
Error
(
err
))
}
else
{
blob
=
thumbnailBlob
}
}
c
.
Response
()
.
Writer
.
Header
()
.
Set
(
echo
.
HeaderCacheControl
,
"max-age=31536000, immutable"
)
c
.
Response
()
.
Writer
.
Header
()
.
Set
(
echo
.
HeaderContentSecurityPolicy
,
"default-src 'self'"
)
resourceType
:=
strings
.
ToLower
(
resource
.
Type
)
if
strings
.
HasPrefix
(
resourceType
,
"text"
)
{
resourceType
=
echo
.
MIMETextPlainCharsetUTF8
}
else
if
strings
.
HasPrefix
(
resourceType
,
"video"
)
||
strings
.
HasPrefix
(
resourceType
,
"audio"
)
{
http
.
ServeContent
(
c
.
Response
(),
c
.
Request
(),
resource
.
Filename
,
time
.
Unix
(
resource
.
UpdatedTs
,
0
),
bytes
.
NewReader
(
blob
))
return
nil
}
c
.
Response
()
.
Writer
.
Header
()
.
Set
(
"Content-Disposition"
,
fmt
.
Sprintf
(
`filename="%s"`
,
resource
.
Filename
))
return
c
.
Stream
(
http
.
StatusOK
,
resourceType
,
bytes
.
NewReader
(
blob
))
}
func
(
s
*
APIV1Service
)
createResourceCreateActivity
(
ctx
context
.
Context
,
resource
*
store
.
Resource
)
error
{
payload
:=
ActivityResourceCreatePayload
{
Filename
:
resource
.
Filename
,
...
...
@@ -495,51 +401,6 @@ func replacePathTemplate(path, filename string) string {
return
path
}
var
availableGeneratorAmount
int32
=
32
func
getOrGenerateThumbnailImage
(
srcBlob
[]
byte
,
dstPath
string
)
([]
byte
,
error
)
{
if
_
,
err
:=
os
.
Stat
(
dstPath
);
err
!=
nil
{
if
!
errors
.
Is
(
err
,
os
.
ErrNotExist
)
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to check thumbnail image stat"
)
}
if
atomic
.
LoadInt32
(
&
availableGeneratorAmount
)
<=
0
{
return
nil
,
errors
.
New
(
"not enough available generator amount"
)
}
atomic
.
AddInt32
(
&
availableGeneratorAmount
,
-
1
)
defer
func
()
{
atomic
.
AddInt32
(
&
availableGeneratorAmount
,
1
)
}()
reader
:=
bytes
.
NewReader
(
srcBlob
)
src
,
err
:=
imaging
.
Decode
(
reader
,
imaging
.
AutoOrientation
(
true
))
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to decode thumbnail image"
)
}
thumbnailImage
:=
imaging
.
Resize
(
src
,
512
,
0
,
imaging
.
Lanczos
)
dstDir
:=
path
.
Dir
(
dstPath
)
if
err
:=
os
.
MkdirAll
(
dstDir
,
os
.
ModePerm
);
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to create thumbnail dir"
)
}
if
err
:=
imaging
.
Save
(
thumbnailImage
,
dstPath
);
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to resize thumbnail image"
)
}
}
dstFile
,
err
:=
os
.
Open
(
dstPath
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to open the local resource"
)
}
defer
dstFile
.
Close
()
dstBlob
,
err
:=
io
.
ReadAll
(
dstFile
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to read the local resource"
)
}
return
dstBlob
,
nil
}
func
convertResourceFromStore
(
resource
*
store
.
Resource
)
*
Resource
{
return
&
Resource
{
ID
:
resource
.
ID
,
...
...
api/v1/v1.go
View file @
31997936
...
...
@@ -3,6 +3,7 @@ package v1
import
(
"github.com/labstack/echo/v4"
"github.com/usememos/memos/api/resource"
"github.com/usememos/memos/plugin/telegram"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
...
...
@@ -66,7 +67,9 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) {
return
JWTMiddleware
(
s
,
next
,
s
.
Secret
)
})
s
.
registerGetterPublicRoutes
(
publicGroup
)
s
.
registerResourcePublicRoutes
(
publicGroup
)
// Create and register resource public routes.
resourceService
:=
resource
.
NewService
(
s
.
Profile
,
s
.
Store
)
resourceService
.
RegisterResourcePublicRoutes
(
publicGroup
)
// programmatically set API version same as the server version
SwaggerInfo
.
Version
=
s
.
Profile
.
Version
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment