Merge pull request #213 from mattermost/file-permissions
Store files per workspace and root block
This commit is contained in:
commit
db6d496853
10 changed files with 323 additions and 127 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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 || ''
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 ''
|
||||
|
|
Loading…
Reference in a new issue