1

I'm making a website to display a project that I've got. About once a week, this project generates a new release which has a file machine_friendly.csv. This file just contains some generated data (and is different every time).

I want to create a github pages website which a user can navigate to and see a prettified version of the machine_friendly.csv file. The problem is that github's CORS doesn't allow me to directly download the file. For example, this doesn't work:

React.useEffect(() => {
    fetch('https://github.com/beyarkay/eskom-calendar/releases/download/latest/machine_friendly.csv')
    .then(response => {
        if (response.ok) return response.json()
        throw new Error('Network response was not ok.')
    })
    .then(data => console.log(data.contents))
    .catch(err => console.log(err));
}, []);

and gives CORS error messages:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at 
https://github.com/beyarkay/eskom-calendar/releases/download/latest/machine_friendly.csv. 
(Reason: CORS header ‘Access-Control-Allow-Origin’ missing). 
Status code: 302.

Is there any way of getting around this? I've tried uploading the file to a pastebin as well as to the github release, but none of the pastebins I've tried enable CORS for free. I've also tried some CORS proxies, but they either take too long or just don't work anymore. I also tried using github's API, but that also gives CORS problems.

Is there a service online that I can upload my small (<1MB) file to and download it later via javascript?

beyarkay
  • 513
  • 3
  • 16

1 Answers1

1

So I kind of found a solution, although it's not great. I ended up messaging the maintainer of dpaste.com who very kindly enabled CORS, so I can now upload my file to dpaste and download it again.

The GH action I've got looks something like:

jobs:
  build-and-publish-calendars:
    runs-on: ubuntu-latest
    steps:
    ...
    - name: POST machine_friendly.csv to dpaste.org
      run: |
        cat calendars/machine_friendly.csv | curl -X POST -F "expires=31536000" -F 'format=url' -F 'content=<-' https://dpaste.org/api/ > pastebin.txt

    - name: Write pastebin link to GH variable
      run: |
        echo "pastebin=$(cat pastebin.txt)/raw" >> $GITHUB_OUTPUT
      id: PASTEBIN

And then later (funky hacks incoming) I include that pastebin link in the description of my release using a personal fork of IsaacShelton/update-existing-release (I maintain the personal fork for some performance improvements which are unrelated to this issue). The step looks like:

    ...
    - name: Update latest release with new calendars
      uses: beyarkay/update-existing-release@master
      with:
        token: ${{ secrets.GH_ACTIONS_PAT }}
        release: My Updated Release
        updateTag: true
        tag: latest
        replace: true
        files: ${{ steps.LS-CALENDARS.outputs.LS_CALENDARS }}
        body: "If you encounter CORS issues, you'll need to use this [pastebin link](${{ steps.PASTEBIN.outputs.pastebin }})"

And in my website, I have a snippet like:

const downloadFromRelease = async () => {

    // We need octokit in order to download the metadata about the release
    const octokit = new Octokit({
        auth: process.env.GH_PAGES_ENV_PAT || process.env.GH_PAGES_PAT
    })
    const desc = await octokit.request("GET /repos/beyarkay/eskom-calendar/releases/72143886", {
        owner: "beyarkay",
        repo: "eskom-calendar",
        release_id: "72143886"
    }).then((res) => res.data.body)

    // Here's some regex that matches the markdown formatted link
    const pastebin_re = /\[pastebin link\]\((https:\/\/dpaste\.org\/(\w+)\/raw)\)/gm
    const match = desc.match(pastebin_re)

    // Now that we've got a match, query that URL to get the data
    const url = match[0].replace("[pastebin link](", "").replace(")", "")
    console.log(`Fetching data from ${url}`)
    return fetch(url)
        .then(res => res.text())
        .then(newEvents => {
            // And finally parse the URL data into Event objects
            const events: Event[] = newEvents.split("\n").map( line => ( {
                area_name: line.split(",")[0],
                start:  line.split(",")[1],
                finsh:  line.split(",")[2],
                stage:  line.split(",")[3],
                source: line.split(",")[4],
            }))
            return events
        })
}

Tying it all together, I can use this snippet to actually setState in react:


   // Define a Result type that represents data which might not be ready yet
    type Result<T, E> = { state: "unsent" }
        | { state: "loading" }
        | { state: "ready", content: T }
        | { state: "error", content: E }
    // The events start out as being "unsent"
    const [events, setEvents] =
        React.useState<Result<Event[], string>>( { state: "unsent" })

    // If they're unsent, then send off the request
    if (events.state === "unsent") {
        downloadMachineFriendly().then(newEvents => {

            // When the data comes back, use the `setEvents` hook
            setEvents({
                state: "ready",
                content: newEvents,
            })

        // If there's an error, store the error
        }).catch(err => setEvents({state: "error", content: err}))
    }

beyarkay
  • 513
  • 3
  • 16
  • 1
    You're right, that is hacky. But at least it works. Thanks! – David Given Apr 09 '23 at 19:50
  • On a sad note, dpaste may not be maintained for much longer ): The company who hosted their servers has kinda isn't around any more: https://github.com/DarrenOfficial/dpaste/issues/232#issuecomment-1501644211 For now it works though – beyarkay Apr 10 '23 at 15:56