diff --git a/src/components/molecules/CurrentDirectoryView/index.tsx b/src/components/molecules/CurrentDirectoryView/index.tsx
index cdf997490a42fc9a0534eecdec3807daded17f76..bd5696bb7cc89293c2630270632fd04ce8458219 100644
--- a/src/components/molecules/CurrentDirectoryView/index.tsx
+++ b/src/components/molecules/CurrentDirectoryView/index.tsx
@@ -2,10 +2,13 @@ import React from "react";
 import Row from "react-bootstrap/esm/Row";
 import Col from "react-bootstrap/esm/Col";
 import ResourceSectionHeader from "../ResourceSectionHeader";
+import { api, methods } from "../../../constants/routes";
+import { request } from "../../../utils/api";
 import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons";
 import { faFileAlt, faFileVideo, faFilePdf, IconDefinition } from "@fortawesome/free-solid-svg-icons";
 import FileCard from "components/atoms/FileCard";
 
+// TODO: Refactor out duplication with QuickAccessView
 export interface CurrentDirectoryViewProps {
   documentItems: {
     title: string;
@@ -13,6 +16,7 @@ export interface CurrentDirectoryViewProps {
     tags: string[];
     id: number;
   }[];
+  moduleCode?: string;
 }
 
 type idBooleanMap = { [key: number]: boolean };
@@ -65,6 +69,49 @@ class CurrentDirectoryView extends React.Component<CurrentDirectoryViewProps, My
     this.setState({ isSelected, isHoveringOver });
   }
 
+  handleDownloadClick() {
+    const onSuccess = (filename: string, data: any) => {
+      data.blob().then((blob: any) => {
+        let url = URL.createObjectURL(blob);
+        let a = document.createElement("a");
+        a.href = url;
+        a.download = filename;
+        a.click();
+        a.remove();
+      });
+    };
+    // Partial application utility
+    const downloadFilename = (filename: string) => {
+      return (data: any) => {
+        return onSuccess(filename, data);
+      };
+    };
+    const onFailure = (error: { text: () => Promise<any> }) => {
+      error.text().then((errorText) => {
+        console.log(errorText);
+      });
+    };
+
+    let indices : number[] = [];
+    for (let key in this.state.isSelected) {
+      if (this.state.isSelected[key]) {
+        indices.push(parseInt(key));
+      }
+    }
+
+    if (indices.length === 1) {
+      // Only one file to download, call single file endpoint
+      let filename = this.props.documentItems.filter(document => document.id === indices[0])[0].title;
+      request(api.MATERIALS_RESOURCES_FILE(indices[0]), methods.GET, downloadFilename(filename), onFailure);
+    } else {
+      // Multiple files to download, call zipped selection endpoint
+      request(api.MATERIALS_ZIPPED_SELECTION, methods.GET, downloadFilename("materials.zip"), onFailure, {
+        ids: indices,
+        course: this.props.moduleCode,
+      });
+    }
+  }
+
   handleSelectAllClick() {
     let items = this.props.documentItems;
     let isSelected = JSON.parse(JSON.stringify(this.state.isSelected));
@@ -76,6 +123,23 @@ class CurrentDirectoryView extends React.Component<CurrentDirectoryViewProps, My
   }
 
   handleCardClick(id: number) {
+    const onSuccess = (data: any) => {
+      data.blob().then((blob: any) => {
+        let url = URL.createObjectURL(blob);
+        let a = document.createElement("a");
+        a.target = "_blank";
+        a.href = url;
+        a.click();
+        a.remove();
+      });
+    };
+    const onFailure = (error: { text: () => Promise<any> }) => {
+      error.text().then((errorText) => {
+        console.log(errorText);
+      });
+		};
+    request(api.MATERIALS_RESOURCES_FILE(id), methods.GET, onSuccess, onFailure);
+
     if (this.isAnySelected()) {
       this.handleIconClick(id);
     }
@@ -99,6 +163,7 @@ class CurrentDirectoryView extends React.Component<CurrentDirectoryViewProps, My
         <ResourceSectionHeader
           heading="Files"
           showDownload={this.isAnySelected()}
+          onDownloadClick={() => this.handleDownloadClick()}
           onSelectAllClick={() => this.handleSelectAllClick()}
 					selectAllIcon={this.isAllSelected() ? faCheckSquare : faSquare}
 					checkBoxColur={this.isAnySelected() ? "#495057" : "#dee2e6"}
diff --git a/src/components/molecules/QuickAccessView/index.tsx b/src/components/molecules/QuickAccessView/index.tsx
index dabff7cdd9ecefb9e51bbfbe805ecdfd2d34212e..22d86264326bab09f073865466df70f5f0333b86 100644
--- a/src/components/molecules/QuickAccessView/index.tsx
+++ b/src/components/molecules/QuickAccessView/index.tsx
@@ -4,6 +4,8 @@ import styles from "./style.module.scss";
 import classNames from "classnames";
 import Row from "react-bootstrap/esm/Row";
 import Col from "react-bootstrap/esm/Col";
+import { api, methods } from "../../../constants/routes";
+import { request } from "../../../utils/api";
 import ResourceSectionHeader from "../ResourceSectionHeader";
 import FileCard from "components/atoms/FileCard";
 import { faSquare, faCheckSquare } from "@fortawesome/free-regular-svg-icons";
@@ -16,6 +18,7 @@ export interface QuickAccessProps {
     tags: string[];
     id: number;
   }[];
+  moduleCode?: string;
 }
 
 type idBooleanMap = { [key: number]: boolean };
@@ -70,6 +73,50 @@ class QuickAccessView extends React.Component<QuickAccessProps, MyState> {
     this.setState({ isSelected, isHoveringOver });
   }
 
+  handleDownloadClick() {
+    const onSuccess = (filename: string, data: any) => {
+      // TODO: Try to navigate straight to the endpoint url instead of creating an object url
+      data.blob().then((blob: any) => {
+        let url = URL.createObjectURL(blob);
+        let a = document.createElement("a");
+        a.href = url;
+        a.download = filename;
+        a.click();
+        a.remove();
+      });
+    };
+    // Partial application utility
+    const downloadFilename = (filename: string) => {
+      return (data: any) => {
+        return onSuccess(filename, data);
+      };
+    };
+    const onFailure = (error: { text: () => Promise<any> }) => {
+      error.text().then((errorText) => {
+        console.log(errorText);
+      });
+    };
+
+    let indices : number[] = [];
+    for (let key in this.state.isSelected) {
+      if (this.state.isSelected[key]) {
+        indices.push(parseInt(key));
+      }
+    }
+
+    if (indices.length === 1) {
+      // Only one file to download, call single file endpoint
+      let filename = this.props.quickAccessItems.filter(document => document.id === indices[0])[0].title;
+      request(api.MATERIALS_RESOURCES_FILE(indices[0]), methods.GET, downloadFilename(filename), onFailure);
+    } else {
+      // Multiple files to download, call zipped selection endpoint
+      request(api.MATERIALS_ZIPPED_SELECTION, methods.GET, downloadFilename("materials.zip"), onFailure, {
+        ids: indices,
+        course: this.props.moduleCode,
+      });
+    }
+  }
+
   handleSelectAllClick() {
     let items = this.props.quickAccessItems;
     let isSelected = JSON.parse(JSON.stringify(this.state.isSelected));
@@ -81,6 +128,24 @@ class QuickAccessView extends React.Component<QuickAccessProps, MyState> {
   }
 
   handleCardClick(id: number) {
+    const onSuccess = (data: any) => {
+      // TODO: Try to navigate straight to the endpoint url instead of creating an object url
+      data.blob().then((blob: any) => {
+        let url = URL.createObjectURL(blob);
+        let a = document.createElement("a");
+        a.target = "_blank";
+        a.href = url;
+        a.click();
+        a.remove();
+      });
+    };
+    const onFailure = (error: { text: () => Promise<any> }) => {
+      error.text().then((errorText) => {
+        console.log(errorText);
+      });
+		};
+    request(api.MATERIALS_RESOURCES_FILE(id), methods.GET, onSuccess, onFailure);
+
     if (this.isAnySelected()) {
       this.handleIconClick(id);
     }
@@ -103,6 +168,7 @@ class QuickAccessView extends React.Component<QuickAccessProps, MyState> {
       <>
         <ResourceSectionHeader
           heading="Quick Access"
+          onDownloadClick={() => this.handleDownloadClick()}
           showDownload={this.isAnySelected()}
           onSelectAllClick={() => this.handleSelectAllClick()}
           selectAllIcon={this.isAllSelected() ? faCheckSquare : faSquare}
diff --git a/src/components/molecules/ResourceSectionHeader/index.tsx b/src/components/molecules/ResourceSectionHeader/index.tsx
index cc89bdc5b717b609e0b56414cce7e86b2e0cc426..7eccc3006ceeee6ca82ce869bda3f54bb383cbad 100644
--- a/src/components/molecules/ResourceSectionHeader/index.tsx
+++ b/src/components/molecules/ResourceSectionHeader/index.tsx
@@ -10,6 +10,7 @@ export interface SectionHeaderProps {
   heading: string;
   selectAllIcon: IconDefinition;
   showDownload: Boolean;
+	onDownloadClick: (event: React.MouseEvent) => void;
 	onSelectAllClick: (event: React.MouseEvent) => void;
 	checkBoxColur: string;
 }
@@ -18,6 +19,7 @@ const ResourceSectionHeader: React.FC<SectionHeaderProps> = ({
   heading,
   showDownload,
   selectAllIcon,
+  onDownloadClick,
 	onSelectAllClick,
 	checkBoxColur,
 }: SectionHeaderProps) => {
@@ -37,7 +39,7 @@ const ResourceSectionHeader: React.FC<SectionHeaderProps> = ({
 								style={{ color: checkBoxColur }}
 								variant="secondary" 
                 className={styles.sectionHeaderButton}
-                onClick={() => {}}
+                onClick={onDownloadClick}
               >
                 <FontAwesomeIcon
                   className={styles.buttonIcon}
diff --git a/src/components/molecules/ResourcesFolderView/index.tsx b/src/components/molecules/ResourcesFolderView/index.tsx
index b4fdc8c9d9bb65fbfee50190f7694cd78f27c4de..4184388fbbd0f3a18a7f0fa2c42766c3a8991f78 100644
--- a/src/components/molecules/ResourcesFolderView/index.tsx
+++ b/src/components/molecules/ResourcesFolderView/index.tsx
@@ -116,6 +116,7 @@ class ResourcesFolderView extends React.Component<PropsType, MyState> {
         <ResourceSectionHeader
           heading="Folders"
           showDownload={this.isAnySelected()}
+          onDownloadClick={() => {}}
           onSelectAllClick={() => this.handleSelectAllClick()}
 					selectAllIcon={this.isAllSelected() ? faCheckSquare : faSquare}
 					checkBoxColur={this.isAnySelected() ? "#495057" : "#dee2e6"}
diff --git a/src/components/pages/ModuleResources/index.tsx b/src/components/pages/ModuleResources/index.tsx
index 0283b05eb7cfc85efc0eb6d79dd422846aca1d45..75476cffbfa23155902d05ec27e127c82bf25cff 100644
--- a/src/components/pages/ModuleResources/index.tsx
+++ b/src/components/pages/ModuleResources/index.tsx
@@ -2,7 +2,7 @@ import React from "react";
 import styles from "./style.module.scss";
 
 import { request } from "../../../utils/api";
-import { api } from "../../../constants/routes";
+import { api, methods } from "../../../constants/routes";
 import MyBreadcrumbs from "components/atoms/MyBreadcrumbs";
 
 import InputGroup from "react-bootstrap/InputGroup";
@@ -46,11 +46,12 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> {
     };
   }
 
+  moduleCode = this.props.moduleID.startsWith("CO")
+  ? this.props.moduleID.slice(2)
+  : this.props.moduleID;
+
   componentDidMount() {
     this.setState({ isLoaded: false });
-    let moduleCode = this.props.moduleID.startsWith("CO")
-      ? this.props.moduleID.slice(2)
-      : this.props.moduleID;
     const onSuccess = (data: { json: () => Promise<any> }) => {
       let resourceArr: Resource[] = [];
 
@@ -75,9 +76,9 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> {
       });
     };
 
-    request(api.MATERIALS_RESOURCES, "GET", onSuccess, onFailure, {
+    request(api.MATERIALS_RESOURCES, methods.GET, onSuccess, onFailure, {
       year: this.props.year,
-      course: moduleCode,
+      course: this.moduleCode,
     });
   }
 
@@ -170,12 +171,12 @@ class ModuleResources extends React.Component<ResourcesProps, ResourceState> {
                 <ResourcesFolderView folderItems={folders} />
               ) : null}
               {scope !== "" || this.state.searchText !== "" ? (
-                <CurrentDirectoryView documentItems={filesContent} />
+                <CurrentDirectoryView documentItems={filesContent} moduleCode={this.moduleCode}/>
               ) : null}
               {this.state.searchText === "" &&
               scope === "" &&
               quickAccessItems.length > 0 ? (
-                <QuickAccessView quickAccessItems={quickAccessItems} />
+                <QuickAccessView quickAccessItems={quickAccessItems} moduleCode={this.moduleCode}/>
               ) : null}
             </>
           )
diff --git a/src/constants/routes.tsx b/src/constants/routes.tsx
index dd37f6e245d5e0516af3a876b3a631900a99b536..0781c6598444c590e0aed35bf0af81a33fa87ddf 100644
--- a/src/constants/routes.tsx
+++ b/src/constants/routes.tsx
@@ -8,8 +8,15 @@ const prod = {
 
 const config = process.env.NODE_ENV === "development" ? dev : prod;
 
+export const methods = {
+  GET: "GET",
+  POST: "POST",
+}
+
 export const api = {
   MATERIALS_LOGIN: config.MATERIALS_URL + "/auth/login",
   MATERIALS_COURSES: config.MATERIALS_URL + "/courses/1819",
-  MATERIALS_RESOURCES: config.MATERIALS_URL + "/resources"
+  MATERIALS_RESOURCES: config.MATERIALS_URL + "/resources",
+  MATERIALS_RESOURCES_FILE: (id: number) => { return config.MATERIALS_URL + "/resources/" + id + "/file"; },
+  MATERIALS_ZIPPED_SELECTION: config.MATERIALS_URL + "/resources/zipped/selection",
 }
\ No newline at end of file
diff --git a/src/utils/api.tsx b/src/utils/api.tsx
index b21d81177ec0c48f5daa7c51ac1f72f2f25990f3..e84733d16781c7e2e07fa5bdd0a076eb0d15ea98 100644
--- a/src/utils/api.tsx
+++ b/src/utils/api.tsx
@@ -1,6 +1,6 @@
 import authConstants from "../constants/auth";
 import authenticationService from "../utils/auth";
-import { api } from "../constants/routes"
+import { api, methods } from "../constants/routes"
 
 interface RequestOptions {
     [key: string]: any
@@ -26,7 +26,7 @@ export async function request(url: string, method: string, onSuccess: any, onErr
     },
   };
 
-  if (method === "GET") {
+  if (method === methods.GET) {
     url = url + "?" + new URLSearchParams(body);
   } else {
     options.body = JSON.stringify(body);