Merge pull request #213 from mattermost/file-permissions

Store files per workspace and root block
This commit is contained in:
Chen-I Lim 2021-03-30 10:10:29 -07:00 committed by GitHub
commit db6d496853
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 323 additions and 127 deletions

View file

@ -82,12 +82,12 @@ func (a *API) RegisterRoutes(r *mux.Router) {
apiv1.HandleFunc("/login", a.handleLogin).Methods("POST")
apiv1.HandleFunc("/register", a.handleRegister).Methods("POST")
apiv1.HandleFunc("/files", a.sessionRequired(a.handleUploadFile)).Methods("POST")
apiv1.HandleFunc("/workspaces/{workspaceID}/{rootID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST")
// Get Files API
files := r.PathPrefix("/files").Subrouter()
files.HandleFunc("/{filename}", a.sessionRequired(a.handleServeFile)).Methods("GET")
files.HandleFunc("/workspaces/{workspaceID}/{rootID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET")
}
func (a *API) RegisterAdminRoutes(r *mux.Router) {
@ -116,15 +116,47 @@ func (a *API) checkCSRFToken(r *http.Request) bool {
return false
}
func (a *API) hasValidReadTokenForBlock(r *http.Request, container store.Container, blockID string) bool {
query := r.URL.Query()
readToken := query.Get("read_token")
if len(readToken) < 1 {
return false
}
isValid, err := a.app().IsValidReadToken(container, blockID, readToken)
if err != nil {
log.Printf("IsValidReadToken ERROR: %v", err)
return false
}
return isValid
}
func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID string) (*store.Container, error) {
ctx := r.Context()
session, _ := ctx.Value("session").(*model.Session)
if a.WorkspaceAuthenticator == nil {
// Native auth: always use root workspace
container := store.Container{
WorkspaceID: "",
}
return &container, nil
// Has session
if session != nil {
return &container, nil
}
// No session, but has valid read token (read-only mode)
if len(blockID) > 0 && a.hasValidReadTokenForBlock(r, container, blockID) {
return &container, nil
}
return nil, errors.New("Access denied to workspace")
}
// Workspace auth
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
@ -137,34 +169,17 @@ func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID str
WorkspaceID: workspaceID,
}
ctx := r.Context()
session, _ := ctx.Value("session").(*model.Session)
if session == nil && len(blockID) > 0 {
// No session, check for read_token
query := r.URL.Query()
readToken := query.Get("read_token")
// Require read token
if len(readToken) < 1 {
return nil, errors.New("Access denied to workspace")
}
isValid, err := a.app().IsValidReadToken(container, blockID, readToken)
if err != nil {
log.Printf("IsValidReadToken ERROR: %v", err)
return nil, errors.New("Access denied to workspace")
}
if !isValid {
return nil, errors.New("Access denied to workspace")
}
} else {
if !a.WorkspaceAuthenticator.DoesUserHaveWorkspaceAccess(session, workspaceID) {
return nil, errors.New("Access denied to workspace")
}
// Has session and access to workspace
if session != nil && a.WorkspaceAuthenticator.DoesUserHaveWorkspaceAccess(session, container.WorkspaceID) {
return &container, nil
}
return &container, nil
// No session, but has valid read token (read-only mode)
if len(blockID) > 0 && a.hasValidReadTokenForBlock(r, container, blockID) {
return &container, nil
}
return nil, errors.New("Access denied to workspace")
}
func (a *API) getContainer(r *http.Request) (*store.Container, error) {
@ -956,7 +971,7 @@ func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r
// File upload
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /files/{fileID} getFile
// swagger:operation GET /workspaces/{workspaceID}/{rootID}/{fileID} getFile
//
// Returns the contents of an uploaded file
//
@ -966,6 +981,16 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
// - image/jpg
// - image/png
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: rootID
// in: path
// description: ID of the root block
// required: true
// type: string
// - name: fileID
// in: path
// description: ID of the file
@ -982,8 +1007,17 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
rootID := vars["rootID"]
filename := vars["filename"]
// Caller must have access to the root block's container
_, err := a.getContainerAllowingReadTokenForBlock(r, rootID)
if err != nil {
noContainerErrorResponse(w, err)
return
}
contentType := "image/jpg"
fileExtension := strings.ToLower(filepath.Ext(filename))
@ -993,7 +1027,7 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", contentType)
filePath := a.app().GetFilePath(filename)
filePath := a.app().GetFilePath(workspaceID, rootID, filename)
http.ServeFile(w, r, filePath)
}
@ -1006,9 +1040,9 @@ type FileUploadResponse struct {
}
func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/files uploadFile
// swagger:operation POST /api/v1/workspaces/{workspaceID}/{rootID}/files uploadFile
//
// Upload a binary file
// Upload a binary file, attached to a root block
//
// ---
// consumes:
@ -1016,6 +1050,16 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// produces:
// - application/json
// parameters:
// - name: workspaceID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: rootID
// in: path
// description: ID of the root block
// required: true
// type: string
// - name: uploaded file
// in: formData
// type: file
@ -1032,6 +1076,17 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
rootID := vars["rootID"]
// Caller must have access to the root block's container
_, err := a.getContainerAllowingReadTokenForBlock(r, rootID)
if err != nil {
noContainerErrorResponse(w, err)
return
}
file, handle, err := r.FormFile("file")
if err != nil {
fmt.Fprintf(w, "%v", err)
@ -1040,7 +1095,7 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
}
defer file.Close()
fileId, err := a.app().SaveFile(file, handle.Filename)
fileId, err := a.app().SaveFile(file, workspaceID, rootID, handle.Filename)
if err != nil {
errorResponse(w, http.StatusInternalServerError, "", err)
return

View file

@ -10,7 +10,7 @@ import (
"github.com/mattermost/focalboard/server/utils"
)
func (a *App) SaveFile(reader io.Reader, filename string) (string, error) {
func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (string, error) {
// NOTE: File extension includes the dot
fileExtension := strings.ToLower(filepath.Ext(filename))
if fileExtension == ".jpeg" {
@ -18,8 +18,9 @@ func (a *App) SaveFile(reader io.Reader, filename string) (string, error) {
}
createdFilename := fmt.Sprintf(`%s%s`, utils.CreateGUID(), fileExtension)
filePath := filepath.Join(workspaceID, rootID, createdFilename)
_, appErr := a.filesBackend.WriteFile(reader, createdFilename)
_, appErr := a.filesBackend.WriteFile(reader, filePath)
if appErr != nil {
return "", errors.New("unable to store the file in the files storage")
}
@ -27,8 +28,9 @@ func (a *App) SaveFile(reader io.Reader, filename string) (string, error) {
return createdFilename, nil
}
func (a *App) GetFilePath(filename string) string {
func (a *App) GetFilePath(workspaceID, rootID, filename string) string {
folderPath := a.config.FilesPath
rootPath := filepath.Join(folderPath, workspaceID, rootID)
return filepath.Join(folderPath, filename)
return filepath.Join(rootPath, filename)
}

View file

@ -3160,7 +3160,7 @@ Type of blocks to return, omit to specify all types
<p class="marked">Returns the contents of an uploaded file</p>
<p></p>
<br />
<pre class="prettyprint language-html prettyprinted" data-type="get"><code><span class="pln">/files/{fileID}</span></code></pre>
<pre class="prettyprint language-html prettyprinted" data-type="get"><code><span class="pln">/workspaces/{workspaceID}/{rootID}/{fileID}</span></code></pre>
<p>
<h3>Usage and SDK Samples</h3>
</p>
@ -3184,7 +3184,7 @@ Type of blocks to return, omit to specify all types
<pre class="prettyprint"><code class="language-bsh">curl -X GET\
-H "Authorization: [[apiKey]]"\
-H "Accept: application/json,image/jpg,image/png"\
"http://localhost/api/v1/files/{fileID}"</code></pre>
"http://localhost/api/v1/workspaces/{workspaceID}/{rootID}/{fileID}"</code></pre>
</div>
<div class="tab-pane" id="examples-Default-getFile-0-java">
<pre class="prettyprint"><code class="language-java">import org.openapitools.client.*;
@ -3207,10 +3207,12 @@ public class DefaultApiExample {
// Create an instance of the API class
DefaultApi apiInstance = new DefaultApi();
String workspaceID = workspaceID_example; // String | Workspace ID
String rootID = rootID_example; // String | ID of the root block
String fileID = fileID_example; // String | ID of the file
try {
apiInstance.getFile(fileID);
apiInstance.getFile(workspaceID, rootID, fileID);
} catch (ApiException e) {
System.err.println("Exception when calling DefaultApi#getFile");
e.printStackTrace();
@ -3226,10 +3228,12 @@ public class DefaultApiExample {
public class DefaultApiExample {
public static void main(String[] args) {
DefaultApi apiInstance = new DefaultApi();
String workspaceID = workspaceID_example; // String | Workspace ID
String rootID = rootID_example; // String | ID of the root block
String fileID = fileID_example; // String | ID of the file
try {
apiInstance.getFile(fileID);
apiInstance.getFile(workspaceID, rootID, fileID);
} catch (ApiException e) {
System.err.println("Exception when calling DefaultApi#getFile");
e.printStackTrace();
@ -3252,9 +3256,13 @@ public class DefaultApiExample {
// Create an instance of the API class
DefaultApi *apiInstance = [[DefaultApi alloc] init];
String *workspaceID = workspaceID_example; // Workspace ID (default to null)
String *rootID = rootID_example; // ID of the root block (default to null)
String *fileID = fileID_example; // ID of the file (default to null)
[apiInstance getFileWith:fileID
[apiInstance getFileWith:workspaceID
rootID:rootID
fileID:fileID
completionHandler: ^(NSError* error) {
if (error) {
NSLog(@"Error: %@", error);
@ -3275,6 +3283,8 @@ BearerAuth.apiKey = "YOUR API KEY";
// Create an instance of the API class
var api = new FocalboardServer.DefaultApi()
var workspaceID = workspaceID_example; // {String} Workspace ID
var rootID = rootID_example; // {String} ID of the root block
var fileID = fileID_example; // {String} ID of the file
var callback = function(error, data, response) {
@ -3284,7 +3294,7 @@ var callback = function(error, data, response) {
console.log('API called successfully.');
}
};
api.getFile(fileID, callback);
api.getFile(workspaceID, rootID, fileID, callback);
</code></pre>
</div>
@ -3311,10 +3321,12 @@ namespace Example
// Create an instance of the API class
var apiInstance = new DefaultApi();
var workspaceID = workspaceID_example; // String | Workspace ID (default to null)
var rootID = rootID_example; // String | ID of the root block (default to null)
var fileID = fileID_example; // String | ID of the file (default to null)
try {
apiInstance.getFile(fileID);
apiInstance.getFile(workspaceID, rootID, fileID);
} catch (Exception e) {
Debug.Print("Exception when calling DefaultApi.getFile: " + e.Message );
}
@ -3335,10 +3347,12 @@ OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authori
// Create an instance of the API class
$api_instance = new OpenAPITools\Client\Api\DefaultApi();
$workspaceID = workspaceID_example; // String | Workspace ID
$rootID = rootID_example; // String | ID of the root block
$fileID = fileID_example; // String | ID of the file
try {
$api_instance->getFile($fileID);
$api_instance->getFile($workspaceID, $rootID, $fileID);
} catch (Exception $e) {
echo 'Exception when calling DefaultApi->getFile: ', $e->getMessage(), PHP_EOL;
}
@ -3357,10 +3371,12 @@ $WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
# Create an instance of the API class
my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
my $workspaceID = workspaceID_example; # String | Workspace ID
my $rootID = rootID_example; # String | ID of the root block
my $fileID = fileID_example; # String | ID of the file
eval {
$api_instance->getFile(fileID => $fileID);
$api_instance->getFile(workspaceID => $workspaceID, rootID => $rootID, fileID => $fileID);
};
if ($@) {
warn "Exception when calling DefaultApi->getFile: $@\n";
@ -3381,10 +3397,12 @@ openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
# Create an instance of the API class
api_instance = openapi_client.DefaultApi()
workspaceID = workspaceID_example # String | Workspace ID (default to null)
rootID = rootID_example # String | ID of the root block (default to null)
fileID = fileID_example # String | ID of the file (default to null)
try:
api_instance.get_file(fileID)
api_instance.get_file(workspaceID, rootID, fileID)
except ApiException as e:
print("Exception when calling DefaultApi->getFile: %s\n" % e)</code></pre>
</div>
@ -3393,10 +3411,12 @@ except ApiException as e:
<pre class="prettyprint"><code class="language-rust">extern crate DefaultApi;
pub fn main() {
let workspaceID = workspaceID_example; // String
let rootID = rootID_example; // String
let fileID = fileID_example; // String
let mut context = DefaultApi::Context::default();
let result = client.getFile(fileID, &context).wait();
let result = client.getFile(workspaceID, rootID, fileID, &context).wait();
println!("{:?}", result);
}
@ -3417,6 +3437,52 @@ pub fn main() {
<th width="150px">Name</th>
<th>Description</th>
</tr>
<tr><td style="width:150px;">workspaceID*</td>
<td>
<div id="d2e199_getFile_workspaceID">
<div class="json-schema-view">
<div class="primitive">
<span class="type">
String
</span>
<div class="inner description marked">
Workspace ID
</div>
</div>
<div class="inner required">
Required
</div>
</div>
</div>
</td>
</tr>
<tr><td style="width:150px;">rootID*</td>
<td>
<div id="d2e199_getFile_rootID">
<div class="json-schema-view">
<div class="primitive">
<span class="type">
String
</span>
<div class="inner description marked">
ID of the root block
</div>
</div>
<div class="inner required">
Required
</div>
</div>
</div>
</td>
</tr>
<tr><td style="width:150px;">fileID*</td>
<td>
@ -8490,10 +8556,10 @@ $(document).ready(function() {
<div class="pull-right"></div>
<div class="clearfix"></div>
<p></p>
<p class="marked">Upload a binary file</p>
<p class="marked">Upload a binary file, attached to a root block</p>
<p></p>
<br />
<pre class="prettyprint language-html prettyprinted" data-type="post"><code><span class="pln">/api/v1/files</span></code></pre>
<pre class="prettyprint language-html prettyprinted" data-type="post"><code><span class="pln">/api/v1/workspaces/{workspaceID}/{rootID}/files</span></code></pre>
<p>
<h3>Usage and SDK Samples</h3>
</p>
@ -8518,7 +8584,7 @@ $(document).ready(function() {
-H "Authorization: [[apiKey]]"\
-H "Accept: application/json"\
-H "Content-Type: multipart/form-data"\
"http://localhost/api/v1/api/v1/files"</code></pre>
"http://localhost/api/v1/api/v1/workspaces/{workspaceID}/{rootID}/files"</code></pre>
</div>
<div class="tab-pane" id="examples-Default-uploadFile-0-java">
<pre class="prettyprint"><code class="language-java">import org.openapitools.client.*;
@ -8541,10 +8607,12 @@ public class DefaultApiExample {
// Create an instance of the API class
DefaultApi apiInstance = new DefaultApi();
String workspaceID = workspaceID_example; // String | Workspace ID
String rootID = rootID_example; // String | ID of the root block
File uploaded file = BINARY_DATA_HERE; // File | The file to upload
try {
FileUploadResponse result = apiInstance.uploadFile(uploaded file);
FileUploadResponse result = apiInstance.uploadFile(workspaceID, rootID, uploaded file);
System.out.println(result);
} catch (ApiException e) {
System.err.println("Exception when calling DefaultApi#uploadFile");
@ -8561,10 +8629,12 @@ public class DefaultApiExample {
public class DefaultApiExample {
public static void main(String[] args) {
DefaultApi apiInstance = new DefaultApi();
String workspaceID = workspaceID_example; // String | Workspace ID
String rootID = rootID_example; // String | ID of the root block
File uploaded file = BINARY_DATA_HERE; // File | The file to upload
try {
FileUploadResponse result = apiInstance.uploadFile(uploaded file);
FileUploadResponse result = apiInstance.uploadFile(workspaceID, rootID, uploaded file);
System.out.println(result);
} catch (ApiException e) {
System.err.println("Exception when calling DefaultApi#uploadFile");
@ -8588,9 +8658,13 @@ public class DefaultApiExample {
// Create an instance of the API class
DefaultApi *apiInstance = [[DefaultApi alloc] init];
String *workspaceID = workspaceID_example; // Workspace ID (default to null)
String *rootID = rootID_example; // ID of the root block (default to null)
File *uploaded file = BINARY_DATA_HERE; // The file to upload (optional) (default to null)
[apiInstance uploadFileWith:uploaded file
[apiInstance uploadFileWith:workspaceID
rootID:rootID
uploaded file:uploaded file
completionHandler: ^(FileUploadResponse output, NSError* error) {
if (output) {
NSLog(@"%@", output);
@ -8614,6 +8688,8 @@ BearerAuth.apiKey = "YOUR API KEY";
// Create an instance of the API class
var api = new FocalboardServer.DefaultApi()
var workspaceID = workspaceID_example; // {String} Workspace ID
var rootID = rootID_example; // {String} ID of the root block
var opts = {
'uploaded file': BINARY_DATA_HERE // {File} The file to upload
};
@ -8625,7 +8701,7 @@ var callback = function(error, data, response) {
console.log('API called successfully. Returned data: ' + data);
}
};
api.uploadFile(opts, callback);
api.uploadFile(workspaceID, rootID, opts, callback);
</code></pre>
</div>
@ -8652,10 +8728,12 @@ namespace Example
// Create an instance of the API class
var apiInstance = new DefaultApi();
var workspaceID = workspaceID_example; // String | Workspace ID (default to null)
var rootID = rootID_example; // String | ID of the root block (default to null)
var uploaded file = BINARY_DATA_HERE; // File | The file to upload (optional) (default to null)
try {
FileUploadResponse result = apiInstance.uploadFile(uploaded file);
FileUploadResponse result = apiInstance.uploadFile(workspaceID, rootID, uploaded file);
Debug.WriteLine(result);
} catch (Exception e) {
Debug.Print("Exception when calling DefaultApi.uploadFile: " + e.Message );
@ -8677,10 +8755,12 @@ OpenAPITools\Client\Configuration::getDefaultConfiguration()->setApiKey('Authori
// Create an instance of the API class
$api_instance = new OpenAPITools\Client\Api\DefaultApi();
$workspaceID = workspaceID_example; // String | Workspace ID
$rootID = rootID_example; // String | ID of the root block
$uploaded file = BINARY_DATA_HERE; // File | The file to upload
try {
$result = $api_instance->uploadFile($uploaded file);
$result = $api_instance->uploadFile($workspaceID, $rootID, $uploaded file);
print_r($result);
} catch (Exception $e) {
echo 'Exception when calling DefaultApi->uploadFile: ', $e->getMessage(), PHP_EOL;
@ -8700,10 +8780,12 @@ $WWW::OPenAPIClient::Configuration::api_key->{'Authorization'} = 'YOUR_API_KEY';
# Create an instance of the API class
my $api_instance = WWW::OPenAPIClient::DefaultApi->new();
my $workspaceID = workspaceID_example; # String | Workspace ID
my $rootID = rootID_example; # String | ID of the root block
my $uploaded file = BINARY_DATA_HERE; # File | The file to upload
eval {
my $result = $api_instance->uploadFile(uploaded file => $uploaded file);
my $result = $api_instance->uploadFile(workspaceID => $workspaceID, rootID => $rootID, uploaded file => $uploaded file);
print Dumper($result);
};
if ($@) {
@ -8725,10 +8807,12 @@ openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
# Create an instance of the API class
api_instance = openapi_client.DefaultApi()
workspaceID = workspaceID_example # String | Workspace ID (default to null)
rootID = rootID_example # String | ID of the root block (default to null)
uploaded file = BINARY_DATA_HERE # File | The file to upload (optional) (default to null)
try:
api_response = api_instance.upload_file(uploaded file=uploaded file)
api_response = api_instance.upload_file(workspaceID, rootID, uploaded file=uploaded file)
pprint(api_response)
except ApiException as e:
print("Exception when calling DefaultApi->uploadFile: %s\n" % e)</code></pre>
@ -8738,10 +8822,12 @@ except ApiException as e:
<pre class="prettyprint"><code class="language-rust">extern crate DefaultApi;
pub fn main() {
let workspaceID = workspaceID_example; // String
let rootID = rootID_example; // String
let uploaded file = BINARY_DATA_HERE; // File
let mut context = DefaultApi::Context::default();
let result = client.uploadFile(uploaded file, &context).wait();
let result = client.uploadFile(workspaceID, rootID, uploaded file, &context).wait();
println!("{:?}", result);
}
@ -8756,6 +8842,59 @@ pub fn main() {
<h2>Parameters</h2>
<div class="methodsubtabletitle">Path parameters</div>
<table id="methodsubtable">
<tr>
<th width="150px">Name</th>
<th>Description</th>
</tr>
<tr><td style="width:150px;">workspaceID*</td>
<td>
<div id="d2e199_uploadFile_workspaceID">
<div class="json-schema-view">
<div class="primitive">
<span class="type">
String
</span>
<div class="inner description marked">
Workspace ID
</div>
</div>
<div class="inner required">
Required
</div>
</div>
</div>
</td>
</tr>
<tr><td style="width:150px;">rootID*</td>
<td>
<div id="d2e199_uploadFile_rootID">
<div class="json-schema-view">
<div class="primitive">
<span class="type">
String
</span>
<div class="inner description marked">
ID of the root block
</div>
</div>
<div class="inner required">
Required
</div>
</div>
</div>
</td>
</tr>
</table>

View file

@ -291,30 +291,6 @@ info:
title: Focalboard Server
version: 1.0.0
paths:
/api/v1/files:
post:
consumes:
- multipart/form-data
description: Upload a binary file
operationId: uploadFile
parameters:
- description: The file to upload
in: formData
name: uploaded file
type: file
produces:
- application/json
responses:
"200":
description: success
schema:
$ref: '#/definitions/FileUploadResponse'
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/api/v1/login:
post:
description: Login user
@ -457,6 +433,40 @@ paths:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/api/v1/workspaces/{workspaceID}/{rootID}/files:
post:
consumes:
- multipart/form-data
description: Upload a binary file, attached to a root block
operationId: uploadFile
parameters:
- description: Workspace ID
in: path
name: workspaceID
required: true
type: string
- description: ID of the root block
in: path
name: rootID
required: true
type: string
- description: The file to upload
in: formData
name: uploaded file
type: file
produces:
- application/json
responses:
"200":
description: success
schema:
$ref: '#/definitions/FileUploadResponse'
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/api/v1/workspaces/{workspaceID}/blocks:
get:
description: Returns blocks
@ -714,11 +724,21 @@ paths:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
/files/{fileID}:
/workspaces/{workspaceID}/{rootID}/{fileID}:
get:
description: Returns the contents of an uploaded file
operationId: getFile
parameters:
- description: Workspace ID
in: path
name: workspaceID
required: true
type: string
- description: ID of the root block
in: path
name: rootID
required: true
type: string
- description: ID of the file
in: path
name: fileID

View file

@ -38,7 +38,7 @@ const AddContentMenuItem = React.memo((props:Props): JSX.Element => {
name={handler.getDisplayText(intl)}
icon={handler.getIcon()}
onClick={async () => {
const newBlock = await handler.createBlock()
const newBlock = await handler.createBlock(card.rootId)
newBlock.parentId = card.id
newBlock.rootId = card.rootId

View file

@ -34,7 +34,7 @@ function addContentMenu(card: Card, intl: IntlShape, type: BlockTypes): JSX.Elem
}
async function addBlock(card: Card, intl: IntlShape, handler: ContentHandler) {
const newBlock = await handler.createBlock()
const newBlock = await handler.createBlock(card.rootId)
newBlock.parentId = card.id
newBlock.rootId = card.rootId

View file

@ -11,7 +11,7 @@ type ContentHandler = {
type: BlockTypes,
getDisplayText: (intl: IntlShape) => string,
getIcon: () => JSX.Element,
createBlock: () => Promise<MutableContentBlock>,
createBlock: (rootId: string) => Promise<MutableContentBlock>,
createComponent: (block: IContentBlock, intl: IntlShape, readonly: boolean) => JSX.Element,
}

View file

@ -17,18 +17,18 @@ type Props = {
const ImageElement = React.memo((props: Props): JSX.Element|null => {
const [imageDataUrl, setImageDataUrl] = useState<string|null>(null)
const {block} = props
useEffect(() => {
if (!imageDataUrl) {
const loadImage = async () => {
const url = await octoClient.getFileAsDataUrl(props.block.fields.fileId)
const url = await octoClient.getFileAsDataUrl(block.rootId, props.block.fields.fileId)
setImageDataUrl(url)
}
loadImage()
}
})
const {block} = props
if (!imageDataUrl) {
return null
}
@ -45,11 +45,11 @@ contentRegistry.registerContentType({
type: 'image',
getDisplayText: (intl) => intl.formatMessage({id: 'ContentBlock.image', defaultMessage: 'image'}),
getIcon: () => <ImageIcon/>,
createBlock: async () => {
createBlock: async (rootId: string) => {
return new Promise<MutableImageBlock>(
(resolve) => {
Utils.selectLocalFile(async (file) => {
const fileId = await octoClient.uploadFile(file)
const fileId = await octoClient.uploadFile(rootId, file)
const block = new MutableImageBlock()
block.fileId = fileId || ''

View file

@ -6,7 +6,6 @@ import {Board, IPropertyOption, IPropertyTemplate, MutableBoard, PropertyType} f
import {BoardView, ISortOption, MutableBoardView} from './blocks/boardView'
import {Card, MutableCard} from './blocks/card'
import {FilterGroup} from './blocks/filterGroup'
import {MutableImageBlock} from './blocks/imageBlock'
import octoClient from './octoClient'
import {OctoUtils} from './octoUtils'
import undoManager from './undomanager'
@ -592,31 +591,6 @@ class Mutator {
return octoClient.importFullArchive(blocks)
}
async createImageBlock(parent: IBlock, file: File, description = 'add image'): Promise<IBlock | undefined> {
const fileId = await octoClient.uploadFile(file)
if (!fileId) {
return undefined
}
const block = new MutableImageBlock()
block.parentId = parent.id
block.rootId = parent.rootId
block.fileId = fileId
await undoManager.perform(
async () => {
await octoClient.insertBlock(block)
},
async () => {
await octoClient.deleteBlock(block.id)
},
description,
this.undoGroupId,
)
return block
}
get canUndo(): boolean {
return undoManager.canUndo
}

View file

@ -17,7 +17,8 @@ class OctoClient {
set token(value: string) {
localStorage.setItem('sessionId', value)
}
get readToken(): string {
private readToken(): string {
const queryString = new URLSearchParams(window.location.search)
const readToken = queryString.get('r') || ''
return readToken
@ -123,8 +124,9 @@ class OctoClient {
async getSubtree(rootId?: string, levels = 2): Promise<IBlock[]> {
let path = this.workspacePath() + `/blocks/${encodeURIComponent(rootId || '')}/subtree?l=${levels}`
if (this.readToken) {
path += `&read_token=${this.readToken}`
const readToken = this.readToken()
if (readToken) {
path += `&read_token=${readToken}`
}
const response = await fetch(this.serverUrl + path, {headers: this.headers()})
if (response.status !== 200) {
@ -308,7 +310,7 @@ class OctoClient {
// Files
// Returns fileId of uploaded file, or undefined on failure
async uploadFile(file: File): Promise<string | undefined> {
async uploadFile(rootID: string, file: File): Promise<string | undefined> {
// IMPORTANT: We need to post the image as a form. The browser will convert this to a application/x-www-form-urlencoded POST
const formData = new FormData()
formData.append('file', file)
@ -319,7 +321,7 @@ class OctoClient {
// TIPTIP: Leave out Content-Type here, it will be automatically set by the browser
delete headers['Content-Type']
const response = await fetch(this.serverUrl + '/api/v1/files', {
const response = await fetch(this.serverUrl + this.workspacePath() + '/' + rootID + '/files', {
method: 'POST',
headers,
body: formData,
@ -345,8 +347,12 @@ class OctoClient {
return undefined
}
async getFileAsDataUrl(fileId: string): Promise<string> {
const path = '/files/' + fileId
async getFileAsDataUrl(rootId: string, fileId: string): Promise<string> {
let path = '/files/workspaces/' + this.workspaceId + '/' + rootId + '/' + fileId
const readToken = this.readToken()
if (readToken) {
path += `?read_token=${readToken}`
}
const response = await fetch(this.serverUrl + path, {headers: this.headers()})
if (response.status !== 200) {
return ''