How to upload attachment in pure Javascript? (with HTML macro)

Peter Van Gerwen May 13, 2020

Dear,
In the HTML macro on a confluence page, I managed to get data from an attached excel file in a javascript variable. I could then do all sorts of manipulation. However, I did not manage to upload the processed file back to the attachments. Here is what I did already:

<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.0/xlsx.full.min.js"></script>
var xldata; var workbook; var firstSheet; var result; var fresult=[]; var wb; var ws; var outData;
var url2="http://xxxxxxxx.com/download/attachments/xxxxxxx/Test.xlsx";
var request2 = new XMLHttpRequest();
request2.responseType = "arraybuffer";
request2.open("GET", url2, true);
request2.send(null);
request2.onload = function() {xldata = new Uint8Array(request2.response);}

 

With the javascript from the XLSX library, I could turn this into a new excel file.

// read the data from xldata into an XLSX object
workbook = XLSX.read(xldata , {type: 'array'});
// get a handle to the first sheet
firstSheet = workbook.Sheets[workbook.SheetNames[0]];
// put the data of the first sheet into a javascript variable result
result = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
//put the array into a structure (given the first row was a HEADER row)
result.forEach(function(row,i){if (i>0){var trow={}; row.forEach(function(elm,j){trow[result[0][j]]=elm});fresult.push(trow)}});

//create a new workbook
wb=XLSX.utils.book_new();
// create a new worksheet into the workbook and append a sheet
ws=XLSX.utils.json_to_sheet(fresult);
XLSX.utils.book_append_sheet(wb,ws,"NieuwLanden");
document.getElementById("landen").innerHTML="fresult: \n"+JSON.stringify(fresult);

 

So far so good, everything is working as expected!
There are two ways I can turn that workbook object into a file

// create variable with the "file" data
var outData = XLSX.write(wb, {bookType:'xlsx', type: 'binary'});
// or create a BLOB (that is automatically downloaded).
var data = XLSX.writeFile(wb, "nnew.xlsx");


The downloaded file is exactly what I expect it to be. A new excel book with the same data.
I would want to have this now - in pure javascript - uploaded to the attachments.
Here is what I tried.

var path="https://xxxxxxxxxxxx.com/rest/api/content/xxxxxxxxxx/child/attachment"
var request3 = new XMLHttpRequest();
request3.open("POST",path,true);
request3.setRequestHeader("X-Atlassian-Token", "nocheck");
request3.send(outData);

Then I get the error
XHR failed loading: POST "https://...../child/attachment"
POST "https://....../child/attachment" net::ERR_CONNECTION_TIMED_OUT


I have no idea what I do wrong....

2 answers

1 accepted

0 votes
Answer accepted
Peter Van Gerwen September 6, 2020

I have finally found the issue. It was twofold.

1. formdata was not properly formed: a) data needed to be in file format, and 2/ I needed to add the 'minorEdit' property

2. In our company, confluence is residing on the intranet with http:// (and not https://). Although now I just use /rest/api/...

So here is what I needed to do:

var outData = XLSX.write(wb, {bookType:'xlsx', type: 'binary'});
var convert = s => {var buf = new ArrayBuffer(s.length); var view = new Uint8Array(buf);for (var i=0; i<s.length; ++i) {view[i] = s.charCodeAt(i) & 0xFF;} return buf}
var dataArray = convert(outData);
var thefile=new File([dataArray],"database.xlsx",{type:"binary"});
path="/rest/api/content/xxxxxxx/child/attachment/xxxxxx/data";

var fdata=new FormData();
fdata.append('file', thefile);
fdata.append('comment', "latest update with fetch");
fdata.append('minorEdit', "true");

var r1=await fetch(path,{
method:"POST",
headers:{"X-Atlassian-Token":"nocheck"},
body:fdata
})
var resp1=await (r1.json());

Now it all works perfectly

0 votes
Bill Bailey
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 14, 2020

When you say HTML macro, what do you mean exactly? User macro? Is this hosted on the Confluence page in question?

Guessing  your are not using a user macro, it would be the better route as you can mix Velocity (which gives you access to Java objects within Confluence), with Javascript (which gives you client-side functionality).

Peter Van Gerwen May 14, 2020

Dear Bill,

When I'm saying HTML macro, I'm talking about using the existing HTML macro on a Confluence Page, which I can access as a user....

With this, I put pure Javascript in the body of the macro. With the code provided above, I can read an excel attachment, attached to a given Confluence page, and do all sorts of data manipulation in javascript, which is working really well. I can show them on the confluence page as I wish. Also, with the inline-javascript, I can even create an excel file, and download it. Again really nice.

I'm trying to find a solution where I can upload that file, from within the HTML macro body. I'm looking for a solution where no server-side changes need to be made...

The XMLHttpRequest shown above allows me to "READ" the attachment, but I do not know how to create a XMLHttpRequest to "SAVE" a new attachement.

Kind regards,

Peter

Bill Bailey
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
May 14, 2020

I might be a security issue that is preventing it from writing. I have a macro that is on a page that does write attachments to another page in Confluence. It is a user macro that uses JavaScript to do the write, but is triggered using a button (taking advantage of the built-in AUI).

Clicking the button then triggers some JavaScript to do the writing of the attachment) using this function

function httpPostFetch(url, data) {
fetch(url, {
method: 'POST',
headers: {
'X-Atlassian-Token': 'no-check'
},
body: data
})
.then(function(response) {
console.log("RESP: " + response.status);
if (response.status == 200) {
AJS.toInit(function() {
AJS.dialog2("#demo-dialog").show();
## Hides the dialog
AJS.$("#dialog-submit-button").click(function (e) {
e.preventDefault();
AJS.dialog2("#demo-dialog").hide();
});
});
}
});
}

 Any JavaScript in your user macro needs to be surrounded by:

<script type="text/javascript">

....
</script>

A user macro has the advantage of allowing it to be configurable via parameters and easily reusable, plus you update the code in one place. And now you have the best of both worlds with server-side access and client-side code.

Peter Van Gerwen May 14, 2020

Dear Bill,

Thank you for your response.

1/ yes I know it needs to be surrounded by <script>. I just copy pasted the main parts. As said, the program works to a point that I can manipulate the data out of the attached excel in javascript, and I can even create a new excel workbook with the manipulated data that can be downloaded to the local drive. So it is up-and-running, and does already about 95% of what I want.

2/ A user macro has for sure advantages, but I want a solution without having to go through the corporate process of getting a server-side macro added.

3/ I assume AUI does the upload. How would the attached code would have to be modified to do it without, i.e. work by using a XMLHttpRequest ?

Peter Van Gerwen May 15, 2020

Dear Bill,

I played a bit with your proposal, but so far without success.

Am I doing something wrong with the data to upload? Are there specific constraints on data? To upload a file, do I need to put it in a new FormData with 'file' and 'data' specified? I tried with the following: 

formdata = new FormData();
formdata.append('file', 'ttest.xlsx');
formdata.append('data', outData);
fetch(path,{method: "POST", "X-Atlassian-Token": "nocheck", "Content-Type":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8", body:formdata})

Kind regards,

Peter

Samuel Zhang
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
December 13, 2020

Hi Peter,

This may no longer be an issue for you, but if anyone else comes across this situation, the formData.append method allows a filename to be specified.

See this code example for a pure Javascript implementation:

formdata = new FormData();
formdata.append('file', outData, 'test.xlsx');
formdata.append('minorEdit', true);
fetch(path,{method: "POST", "X-Atlassian-Token": "nocheck", body: formdata})

In Node.JS, this could look like:

const FormData = require('form-data');

formdata = new FormData();
formdata.append('file', fileBuffer, { filename: 'test.xlsx'} );
formdata.append('minorEdit', true);
fetch(path,
{
method: "POST",
headers: { "X-Atlassian-Token": "nocheck", ...formdata.getHeaders() }
body: formdata
}
);

Cheers,

Samuel 

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events