1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-11-27 08:31:20 +02:00

Store files per workspace and root block

This commit is contained in:
Chen-I Lim 2021-03-29 16:27:35 -07:00
parent 771be97c3e
commit 9ff340c989
10 changed files with 323 additions and 126 deletions

View File

@ -82,12 +82,12 @@ func (a *API) RegisterRoutes(r *mux.Router) {
apiv1.HandleFunc("/login", a.handleLogin).Methods("POST") apiv1.HandleFunc("/login", a.handleLogin).Methods("POST")
apiv1.HandleFunc("/register", a.handleRegister).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 // Get Files API
files := r.PathPrefix("/files").Subrouter() 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) { func (a *API) RegisterAdminRoutes(r *mux.Router) {
@ -116,15 +116,47 @@ func (a *API) checkCSRFToken(r *http.Request) bool {
return false 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) { 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 { if a.WorkspaceAuthenticator == nil {
// Native auth: always use root workspace // Native auth: always use root workspace
container := store.Container{ container := store.Container{
WorkspaceID: "", 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) vars := mux.Vars(r)
workspaceID := vars["workspaceID"] workspaceID := vars["workspaceID"]
@ -137,34 +169,17 @@ func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID str
WorkspaceID: workspaceID, WorkspaceID: workspaceID,
} }
ctx := r.Context() // Has session and access to workspace
session, _ := ctx.Value("session").(*model.Session) if session != nil && a.WorkspaceAuthenticator.DoesUserHaveWorkspaceAccess(session, container.WorkspaceID) {
if session == nil && len(blockID) > 0 { return &container, nil
// 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")
}
} }
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) { func (a *API) getContainer(r *http.Request) (*store.Container, error) {
@ -956,7 +971,7 @@ func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r
// File upload // File upload
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { 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 // Returns the contents of an uploaded file
// //
@ -966,6 +981,16 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
// - image/jpg // - image/jpg
// - image/png // - image/png
// parameters: // 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 // - name: fileID
// in: path // in: path
// description: ID of the file // description: ID of the file
@ -982,8 +1007,17 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
// "$ref": "#/definitions/ErrorResponse" // "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r) vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
rootID := vars["rootID"]
filename := vars["filename"] 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" contentType := "image/jpg"
fileExtension := strings.ToLower(filepath.Ext(filename)) 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) w.Header().Set("Content-Type", contentType)
filePath := a.app().GetFilePath(filename) filePath := a.app().GetFilePath(workspaceID, rootID, filename)
http.ServeFile(w, r, filePath) http.ServeFile(w, r, filePath)
} }
@ -1006,9 +1040,9 @@ type FileUploadResponse struct {
} }
func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { 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: // consumes:
@ -1016,6 +1050,16 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// produces: // produces:
// - application/json // - application/json
// parameters: // 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 // - name: uploaded file
// in: formData // in: formData
// type: file // type: file
@ -1032,6 +1076,17 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// schema: // schema:
// "$ref": "#/definitions/ErrorResponse" // "$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") file, handle, err := r.FormFile("file")
if err != nil { if err != nil {
fmt.Fprintf(w, "%v", err) fmt.Fprintf(w, "%v", err)
@ -1040,7 +1095,7 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
} }
defer file.Close() defer file.Close()
fileId, err := a.app().SaveFile(file, handle.Filename) fileId, err := a.app().SaveFile(file, workspaceID, rootID, handle.Filename)
if err != nil { if err != nil {
errorResponse(w, http.StatusInternalServerError, "", err) errorResponse(w, http.StatusInternalServerError, "", err)
return return

View File

@ -10,7 +10,7 @@ import (
"github.com/mattermost/focalboard/server/utils" "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 // NOTE: File extension includes the dot
fileExtension := strings.ToLower(filepath.Ext(filename)) fileExtension := strings.ToLower(filepath.Ext(filename))
if fileExtension == ".jpeg" { 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) createdFilename := fmt.Sprintf(`%s%s`, utils.CreateGUID(), fileExtension)
filePath := fmt.Sprintf(`%s/%s/%s`, workspaceID, rootID, createdFilename)
_, appErr := a.filesBackend.WriteFile(reader, createdFilename) _, appErr := a.filesBackend.WriteFile(reader, filePath)
if appErr != nil { if appErr != nil {
return "", errors.New("unable to store the file in the files storage") 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 return createdFilename, nil
} }
func (a *App) GetFilePath(filename string) string { func (a *App) GetFilePath(workspaceID, rootID, filename string) string {
folderPath := a.config.FilesPath 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 class="marked">Returns the contents of an uploaded file</p>
<p></p> <p></p>
<br /> <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> <p>
<h3>Usage and SDK Samples</h3> <h3>Usage and SDK Samples</h3>
</p> </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\ <pre class="prettyprint"><code class="language-bsh">curl -X GET\
-H "Authorization: [[apiKey]]"\ -H "Authorization: [[apiKey]]"\
-H "Accept: application/json,image/jpg,image/png"\ -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>
<div class="tab-pane" id="examples-Default-getFile-0-java"> <div class="tab-pane" id="examples-Default-getFile-0-java">
<pre class="prettyprint"><code class="language-java">import org.openapitools.client.*; <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 // Create an instance of the API class
DefaultApi apiInstance = new DefaultApi(); 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 String fileID = fileID_example; // String | ID of the file
try { try {
apiInstance.getFile(fileID); apiInstance.getFile(workspaceID, rootID, fileID);
} catch (ApiException e) { } catch (ApiException e) {
System.err.println("Exception when calling DefaultApi#getFile"); System.err.println("Exception when calling DefaultApi#getFile");
e.printStackTrace(); e.printStackTrace();
@ -3226,10 +3228,12 @@ public class DefaultApiExample {
public class DefaultApiExample { public class DefaultApiExample {
public static void main(String[] args) { public static void main(String[] args) {
DefaultApi apiInstance = new DefaultApi(); 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 String fileID = fileID_example; // String | ID of the file
try { try {
apiInstance.getFile(fileID); apiInstance.getFile(workspaceID, rootID, fileID);
} catch (ApiException e) { } catch (ApiException e) {
System.err.println("Exception when calling DefaultApi#getFile"); System.err.println("Exception when calling DefaultApi#getFile");
e.printStackTrace(); e.printStackTrace();
@ -3252,9 +3256,13 @@ public class DefaultApiExample {
// Create an instance of the API class // Create an instance of the API class
DefaultApi *apiInstance = [[DefaultApi alloc] init]; 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) String *fileID = fileID_example; // ID of the file (default to null)
[apiInstance getFileWith:fileID [apiInstance getFileWith:workspaceID
rootID:rootID
fileID:fileID
completionHandler: ^(NSError* error) { completionHandler: ^(NSError* error) {
if (error) { if (error) {
NSLog(@"Error: %@", error); NSLog(@"Error: %@", error);
@ -3275,6 +3283,8 @@ BearerAuth.apiKey = "YOUR API KEY";
// Create an instance of the API class // Create an instance of the API class
var api = new FocalboardServer.DefaultApi() 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 fileID = fileID_example; // {String} ID of the file
var callback = function(error, data, response) { var callback = function(error, data, response) {
@ -3284,7 +3294,7 @@ var callback = function(error, data, response) {
console.log('API called successfully.'); console.log('API called successfully.');
} }
}; };
api.getFile(fileID, callback); api.getFile(workspaceID, rootID, fileID, callback);
</code></pre> </code></pre>
</div> </div>
@ -3311,10 +3321,12 @@ namespace Example
// Create an instance of the API class // Create an instance of the API class
var apiInstance = new DefaultApi(); 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) var fileID = fileID_example; // String | ID of the file (default to null)
try { try {
apiInstance.getFile(fileID); apiInstance.getFile(workspaceID, rootID, fileID);
} catch (Exception e) { } catch (Exception e) {
Debug.Print("Exception when calling DefaultApi.getFile: " + e.Message ); 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 // Create an instance of the API class
$api_instance = new OpenAPITools\Client\Api\DefaultApi(); $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 $fileID = fileID_example; // String | ID of the file
try { try {
$api_instance->getFile($fileID); $api_instance->getFile($workspaceID, $rootID, $fileID);
} catch (Exception $e) { } catch (Exception $e) {
echo 'Exception when calling DefaultApi->getFile: ', $e->getMessage(), PHP_EOL; 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 # Create an instance of the API class
my $api_instance = WWW::OPenAPIClient::DefaultApi->new(); 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 my $fileID = fileID_example; # String | ID of the file
eval { eval {
$api_instance->getFile(fileID => $fileID); $api_instance->getFile(workspaceID => $workspaceID, rootID => $rootID, fileID => $fileID);
}; };
if ($@) { if ($@) {
warn "Exception when calling DefaultApi->getFile: $@\n"; 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 # Create an instance of the API class
api_instance = openapi_client.DefaultApi() 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) fileID = fileID_example # String | ID of the file (default to null)
try: try:
api_instance.get_file(fileID) api_instance.get_file(workspaceID, rootID, fileID)
except ApiException as e: except ApiException as e:
print("Exception when calling DefaultApi->getFile: %s\n" % e)</code></pre> print("Exception when calling DefaultApi->getFile: %s\n" % e)</code></pre>
</div> </div>
@ -3393,10 +3411,12 @@ except ApiException as e:
<pre class="prettyprint"><code class="language-rust">extern crate DefaultApi; <pre class="prettyprint"><code class="language-rust">extern crate DefaultApi;
pub fn main() { pub fn main() {
let workspaceID = workspaceID_example; // String
let rootID = rootID_example; // String
let fileID = fileID_example; // String let fileID = fileID_example; // String
let mut context = DefaultApi::Context::default(); let mut context = DefaultApi::Context::default();
let result = client.getFile(fileID, &context).wait(); let result = client.getFile(workspaceID, rootID, fileID, &context).wait();
println!("{:?}", result); println!("{:?}", result);
} }
@ -3417,6 +3437,52 @@ pub fn main() {
<th width="150px">Name</th> <th width="150px">Name</th>
<th>Description</th> <th>Description</th>
</tr> </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> <tr><td style="width:150px;">fileID*</td>
<td> <td>
@ -8490,10 +8556,10 @@ $(document).ready(function() {
<div class="pull-right"></div> <div class="pull-right"></div>
<div class="clearfix"></div> <div class="clearfix"></div>
<p></p> <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> <p></p>
<br /> <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> <p>
<h3>Usage and SDK Samples</h3> <h3>Usage and SDK Samples</h3>
</p> </p>
@ -8518,7 +8584,7 @@ $(document).ready(function() {
-H "Authorization: [[apiKey]]"\ -H "Authorization: [[apiKey]]"\
-H "Accept: application/json"\ -H "Accept: application/json"\
-H "Content-Type: multipart/form-data"\ -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>
<div class="tab-pane" id="examples-Default-uploadFile-0-java"> <div class="tab-pane" id="examples-Default-uploadFile-0-java">
<pre class="prettyprint"><code class="language-java">import org.openapitools.client.*; <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 // Create an instance of the API class
DefaultApi apiInstance = new DefaultApi(); 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 File uploaded file = BINARY_DATA_HERE; // File | The file to upload
try { try {
FileUploadResponse result = apiInstance.uploadFile(uploaded file); FileUploadResponse result = apiInstance.uploadFile(workspaceID, rootID, uploaded file);
System.out.println(result); System.out.println(result);
} catch (ApiException e) { } catch (ApiException e) {
System.err.println("Exception when calling DefaultApi#uploadFile"); System.err.println("Exception when calling DefaultApi#uploadFile");
@ -8561,10 +8629,12 @@ public class DefaultApiExample {
public class DefaultApiExample { public class DefaultApiExample {
public static void main(String[] args) { public static void main(String[] args) {
DefaultApi apiInstance = new DefaultApi(); 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 File uploaded file = BINARY_DATA_HERE; // File | The file to upload
try { try {
FileUploadResponse result = apiInstance.uploadFile(uploaded file); FileUploadResponse result = apiInstance.uploadFile(workspaceID, rootID, uploaded file);
System.out.println(result); System.out.println(result);
} catch (ApiException e) { } catch (ApiException e) {
System.err.println("Exception when calling DefaultApi#uploadFile"); System.err.println("Exception when calling DefaultApi#uploadFile");
@ -8588,9 +8658,13 @@ public class DefaultApiExample {
// Create an instance of the API class // Create an instance of the API class
DefaultApi *apiInstance = [[DefaultApi alloc] init]; 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) 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) { completionHandler: ^(FileUploadResponse output, NSError* error) {
if (output) { if (output) {
NSLog(@"%@", output); NSLog(@"%@", output);
@ -8614,6 +8688,8 @@ BearerAuth.apiKey = "YOUR API KEY";
// Create an instance of the API class // Create an instance of the API class
var api = new FocalboardServer.DefaultApi() var api = new FocalboardServer.DefaultApi()
var workspaceID = workspaceID_example; // {String} Workspace ID
var rootID = rootID_example; // {String} ID of the root block
var opts = { var opts = {
'uploaded file': BINARY_DATA_HERE // {File} The file to upload '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); console.log('API called successfully. Returned data: ' + data);
} }
}; };
api.uploadFile(opts, callback); api.uploadFile(workspaceID, rootID, opts, callback);
</code></pre> </code></pre>
</div> </div>
@ -8652,10 +8728,12 @@ namespace Example
// Create an instance of the API class // Create an instance of the API class
var apiInstance = new DefaultApi(); 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) var uploaded file = BINARY_DATA_HERE; // File | The file to upload (optional) (default to null)
try { try {
FileUploadResponse result = apiInstance.uploadFile(uploaded file); FileUploadResponse result = apiInstance.uploadFile(workspaceID, rootID, uploaded file);
Debug.WriteLine(result); Debug.WriteLine(result);
} catch (Exception e) { } catch (Exception e) {
Debug.Print("Exception when calling DefaultApi.uploadFile: " + e.Message ); 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 // Create an instance of the API class
$api_instance = new OpenAPITools\Client\Api\DefaultApi(); $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 $uploaded file = BINARY_DATA_HERE; // File | The file to upload
try { try {
$result = $api_instance->uploadFile($uploaded file); $result = $api_instance->uploadFile($workspaceID, $rootID, $uploaded file);
print_r($result); print_r($result);
} catch (Exception $e) { } catch (Exception $e) {
echo 'Exception when calling DefaultApi->uploadFile: ', $e->getMessage(), PHP_EOL; 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 # Create an instance of the API class
my $api_instance = WWW::OPenAPIClient::DefaultApi->new(); 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 my $uploaded file = BINARY_DATA_HERE; # File | The file to upload
eval { 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); print Dumper($result);
}; };
if ($@) { if ($@) {
@ -8725,10 +8807,12 @@ openapi_client.configuration.api_key['Authorization'] = 'YOUR_API_KEY'
# Create an instance of the API class # Create an instance of the API class
api_instance = openapi_client.DefaultApi() 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) uploaded file = BINARY_DATA_HERE # File | The file to upload (optional) (default to null)
try: 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) pprint(api_response)
except ApiException as e: except ApiException as e:
print("Exception when calling DefaultApi->uploadFile: %s\n" % e)</code></pre> 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; <pre class="prettyprint"><code class="language-rust">extern crate DefaultApi;
pub fn main() { pub fn main() {
let workspaceID = workspaceID_example; // String
let rootID = rootID_example; // String
let uploaded file = BINARY_DATA_HERE; // File let uploaded file = BINARY_DATA_HERE; // File
let mut context = DefaultApi::Context::default(); 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); println!("{:?}", result);
} }
@ -8756,6 +8842,59 @@ pub fn main() {
<h2>Parameters</h2> <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 title: Focalboard Server
version: 1.0.0 version: 1.0.0
paths: 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: /api/v1/login:
post: post:
description: Login user description: Login user
@ -457,6 +433,40 @@ paths:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
security: security:
- BearerAuth: [] - 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: /api/v1/workspaces/{workspaceID}/blocks:
get: get:
description: Returns blocks description: Returns blocks
@ -714,11 +724,21 @@ paths:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
security: security:
- BearerAuth: [] - BearerAuth: []
/files/{fileID}: /workspaces/{workspaceID}/{rootID}/{fileID}:
get: get:
description: Returns the contents of an uploaded file description: Returns the contents of an uploaded file
operationId: getFile operationId: getFile
parameters: 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 - description: ID of the file
in: path in: path
name: fileID name: fileID

View File

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

View File

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

View File

@ -592,31 +592,6 @@ class Mutator {
return octoClient.importFullArchive(blocks) 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 { get canUndo(): boolean {
return undoManager.canUndo return undoManager.canUndo
} }

View File

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