mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
feat(导出文档): 支持取消导出
This commit is contained in:
1 parent
c7ab15baa5
commit
b55e8468ab
7 files changed
+125
-38
No files matched your search
@@ -104,6 +104,7 @@
|
|||||||
<div id="progressPanel" class="progress-panel" role="status" aria-live="polite" hidden>
|
<div id="progressPanel" class="progress-panel" role="status" aria-live="polite" hidden>
|
||||||
<div class="progress-spinner"></div>
|
<div class="progress-spinner"></div>
|
||||||
<div id="progressLabel" class="progress-label">加载中</div>
|
<div id="progressLabel" class="progress-label">加载中</div>
|
||||||
|
<button id="cancelExportButton" class="small-button" type="button" title="取消导出" aria-label="取消导出" hidden>取消</button>
|
||||||
<div class="progress-track">
|
<div class="progress-track">
|
||||||
<div id="progressBar" class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"></div>
|
<div id="progressBar" class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -803,6 +803,7 @@ button.outline-link:hover {
|
|||||||
.form-row[hidden],
|
.form-row[hidden],
|
||||||
.page-list[hidden],
|
.page-list[hidden],
|
||||||
.page-frame[hidden],
|
.page-frame[hidden],
|
||||||
|
.progress-panel > [hidden],
|
||||||
.progress-panel[hidden] {
|
.progress-panel[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -825,6 +826,10 @@ button.outline-link:hover {
|
|||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-panel:has(> button:not([hidden])) {
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-spinner {
|
.progress-spinner {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
|||||||
+31
-5
@@ -28,6 +28,7 @@ const state = {
|
|||||||
wasmRecovering: false,
|
wasmRecovering: false,
|
||||||
wasmRecoveries: 0,
|
wasmRecoveries: 0,
|
||||||
exporting: false,
|
exporting: false,
|
||||||
|
exportRequestID: 0,
|
||||||
ofdBytes: null,
|
ofdBytes: null,
|
||||||
fileName: "ofdgo.ofd",
|
fileName: "ofdgo.ofd",
|
||||||
openSeq: 0,
|
openSeq: 0,
|
||||||
@@ -111,6 +112,7 @@ const el = {
|
|||||||
emptyState: document.querySelector("#emptyState"),
|
emptyState: document.querySelector("#emptyState"),
|
||||||
progressPanel: document.querySelector("#progressPanel"),
|
progressPanel: document.querySelector("#progressPanel"),
|
||||||
progressLabel: document.querySelector("#progressLabel"),
|
progressLabel: document.querySelector("#progressLabel"),
|
||||||
|
cancelExportButton: document.querySelector("#cancelExportButton"),
|
||||||
progressBar: document.querySelector("#progressBar"),
|
progressBar: document.querySelector("#progressBar"),
|
||||||
pageFrame: document.querySelector("#pageFrame"),
|
pageFrame: document.querySelector("#pageFrame"),
|
||||||
viewerPanel: document.querySelector(".viewer-panel"),
|
viewerPanel: document.querySelector(".viewer-panel"),
|
||||||
@@ -214,6 +216,7 @@ el.exportFormat.addEventListener("change", () => updateDPIControl());
|
|||||||
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
||||||
el.exportButton.addEventListener("click", openExportPanel);
|
el.exportButton.addEventListener("click", openExportPanel);
|
||||||
el.exportCancel.addEventListener("click", () => el.exportPanel.close());
|
el.exportCancel.addEventListener("click", () => el.exportPanel.close());
|
||||||
|
el.cancelExportButton.addEventListener("click", cancelExport);
|
||||||
el.exportPanel.addEventListener("pointerdown", (event) => {
|
el.exportPanel.addEventListener("pointerdown", (event) => {
|
||||||
state.exportBackdrop = event.target === el.exportPanel;
|
state.exportBackdrop = event.target === el.exportPanel;
|
||||||
});
|
});
|
||||||
@@ -673,11 +676,14 @@ async function loadWASM() {
|
|||||||
} else if (data.type === "export") {
|
} else if (data.type === "export") {
|
||||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
||||||
if (data.stage === "save") {
|
if (data.stage === "save") {
|
||||||
|
el.cancelExportButton.disabled = true;
|
||||||
setProgress("正在保存", null);
|
setProgress("正在保存", null);
|
||||||
} else if (data.completed === data.total) {
|
} else if (!el.cancelExportButton.disabled) {
|
||||||
setProgress("正在封装", null);
|
if (data.completed === data.total) {
|
||||||
} else {
|
setProgress("正在封装", null);
|
||||||
setProgress(`正在导出 ${data.completed} / ${data.total} 页`, data.completed / data.total * 100);
|
} else {
|
||||||
|
setProgress(`正在导出 ${data.completed} / ${data.total} 页`, data.completed / data.total * 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (data.type === "ready") {
|
} else if (data.type === "ready") {
|
||||||
@@ -691,7 +697,11 @@ async function loadWASM() {
|
|||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
request.resolve(data.data);
|
request.resolve(data.data);
|
||||||
} else {
|
} else {
|
||||||
request.reject(new Error(data.error));
|
const err = new Error(data.error);
|
||||||
|
if (data.canceled) {
|
||||||
|
err.name = "AbortError";
|
||||||
|
}
|
||||||
|
request.reject(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1613,6 +1623,8 @@ async function exportFile(whole, indices = null) {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
state.exporting = false;
|
state.exporting = false;
|
||||||
|
state.exportRequestID = 0;
|
||||||
|
el.cancelExportButton.hidden = true;
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
updateControls();
|
updateControls();
|
||||||
@@ -1620,6 +1632,15 @@ async function exportFile(whole, indices = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelExport() {
|
||||||
|
if (!state.exportRequestID || el.cancelExportButton.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.cancelExportButton.disabled = true;
|
||||||
|
setProgress("正在取消", null);
|
||||||
|
wasmWorker.postMessage({ type: "cancel", id: state.exportRequestID });
|
||||||
|
}
|
||||||
|
|
||||||
function renderPageFlow() {
|
function renderPageFlow() {
|
||||||
el.svgHost.replaceChildren();
|
el.svgHost.replaceChildren();
|
||||||
el.viewerPanel.classList.remove("single-page-fits-height");
|
el.viewerPanel.classList.remove("single-page-fits-height");
|
||||||
@@ -3312,6 +3333,11 @@ async function callWASM(name, ...args) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = ++wasmRequestID;
|
const id = ++wasmRequestID;
|
||||||
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
||||||
|
if (name === "ofdgoExportPage" || name === "ofdgoExportDocument") {
|
||||||
|
state.exportRequestID = id;
|
||||||
|
el.cancelExportButton.hidden = false;
|
||||||
|
el.cancelExportButton.disabled = false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
wasmWorker.postMessage({ id, name, args });
|
wasmWorker.postMessage({ id, name, args });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,38 +1,61 @@
|
|||||||
importScripts("./wasm_exec.js");
|
importScripts("./wasm_exec.js");
|
||||||
|
|
||||||
let pending = Promise.resolve();
|
let pending = Promise.resolve();
|
||||||
|
const exports = new Map();
|
||||||
self.onmessage = ({ data }) => {
|
self.onmessage = ({ data }) => {
|
||||||
|
if (data.type === "cancel") {
|
||||||
|
exports.get(data.id)?.abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument") {
|
||||||
|
exports.set(data.id, new AbortController());
|
||||||
|
}
|
||||||
pending = pending.then(() => handleMessage(data));
|
pending = pending.then(() => handleMessage(data));
|
||||||
};
|
};
|
||||||
async function handleMessage({ id, name, args }) {
|
async function handleMessage({ id, name, args }) {
|
||||||
let output;
|
let output;
|
||||||
|
let channel;
|
||||||
|
const signal = exports.get(id)?.signal;
|
||||||
try {
|
try {
|
||||||
|
signal?.throwIfAborted();
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
let size = 0;
|
let size = 0;
|
||||||
const exporting = name === "ofdgoExportPage" || name === "ofdgoExportDocument";
|
if (signal) {
|
||||||
if (exporting) {
|
|
||||||
const file = args.pop();
|
const file = args.pop();
|
||||||
if (file) {
|
if (file) {
|
||||||
output = await file.createWritable();
|
output = await file.createWritable();
|
||||||
}
|
}
|
||||||
|
signal.throwIfAborted();
|
||||||
|
channel = new MessageChannel();
|
||||||
|
const finish = (done, err) => {
|
||||||
|
channel.port1.onmessage = () => done(err?.message || "", signal.aborted);
|
||||||
|
channel.port2.postMessage(null);
|
||||||
|
};
|
||||||
args.push((bytes, done) => {
|
args.push((bytes, done) => {
|
||||||
|
if (signal.aborted) {
|
||||||
|
finish(done);
|
||||||
|
return;
|
||||||
|
}
|
||||||
size += bytes.length;
|
size += bytes.length;
|
||||||
if (output) {
|
if (output) {
|
||||||
output.write(bytes).then(() => done(), (err) => done(err.message));
|
output.write(bytes).then(() => finish(done), (err) => finish(done, err));
|
||||||
} else {
|
} else {
|
||||||
chunks.push(new Blob([bytes]));
|
chunks.push(new Blob([bytes]));
|
||||||
setTimeout(done, 0);
|
finish(done);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (name === "ofdgoExportDocument") {
|
if (name === "ofdgoExportDocument") {
|
||||||
args.push((completed, total) => {
|
args.push((completed, total, done) => {
|
||||||
self.postMessage({ id, type: "export", stage: "pages", completed, total });
|
self.postMessage({ id, type: "export", stage: "pages", completed, total });
|
||||||
|
finish(done);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const payload = await globalThis[name](...args);
|
const payload = await globalThis[name](...args);
|
||||||
|
signal?.throwIfAborted();
|
||||||
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
||||||
if (exporting && result.ok) {
|
if (signal && result.ok) {
|
||||||
|
exports.delete(id);
|
||||||
result.data.size = size;
|
result.data.size = size;
|
||||||
self.postMessage({ id, type: "export", stage: "save" });
|
self.postMessage({ id, type: "export", stage: "save" });
|
||||||
if (output) {
|
if (output) {
|
||||||
@@ -47,7 +70,16 @@ async function handleMessage({ id, name, args }) {
|
|||||||
self.postMessage({ id, ...result }, result.data?.bytes ? [result.data.bytes.buffer] : []);
|
self.postMessage({ id, ...result }, result.data?.bytes ? [result.data.bytes.buffer] : []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await output?.abort().catch(() => {});
|
await output?.abort().catch(() => {});
|
||||||
self.postMessage({ id, ok: false, error: err.message });
|
const result = { id, ok: false, error: err.message };
|
||||||
|
if (signal?.aborted) {
|
||||||
|
result.error = "导出已取消";
|
||||||
|
result.canceled = true;
|
||||||
|
}
|
||||||
|
self.postMessage(result);
|
||||||
|
} finally {
|
||||||
|
channel?.port1.close();
|
||||||
|
channel?.port2.close();
|
||||||
|
exports.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-14
@@ -18,6 +18,7 @@ package webui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
@@ -34,21 +35,10 @@ type exportWriter struct {
|
|||||||
// 入参: data 导出数据
|
// 入参: data 导出数据
|
||||||
// 返回: int 写入长度, error 错误信息
|
// 返回: int 写入长度, error 错误信息
|
||||||
func (w exportWriter) Write(data []byte) (int, error) {
|
func (w exportWriter) Write(data []byte) (int, error) {
|
||||||
done := make(chan error)
|
|
||||||
callback := js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
||||||
var err error
|
|
||||||
if len(args) > 0 {
|
|
||||||
err = fmt.Errorf("%s", args[0].String())
|
|
||||||
}
|
|
||||||
done <- err
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
defer callback.Release()
|
|
||||||
size := len(data)
|
size := len(data)
|
||||||
for len(data) > 0 {
|
for len(data) > 0 {
|
||||||
n := min(len(data), 1<<20)
|
n := min(len(data), 1<<20)
|
||||||
w.write.Invoke(bytesToJS(data[:n]), callback)
|
if err := awaitExport(w.write, bytesToJS(data[:n])); err != nil {
|
||||||
if err := <-done; err != nil {
|
|
||||||
return size - len(data), err
|
return size - len(data), err
|
||||||
}
|
}
|
||||||
data = data[n:]
|
data = data[n:]
|
||||||
@@ -56,6 +46,26 @@ func (w exportWriter) Write(data []byte) (int, error) {
|
|||||||
return size, nil
|
return size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// awaitExport 等待浏览器处理导出检查点
|
||||||
|
// 入参: fn 浏览器回调, args 回调参数
|
||||||
|
// 返回: error 写入错误或取消原因
|
||||||
|
func awaitExport(fn js.Value, args ...any) error {
|
||||||
|
done := make(chan error)
|
||||||
|
callback := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
var err error
|
||||||
|
if args[1].Bool() {
|
||||||
|
err = context.Canceled
|
||||||
|
} else if message := args[0].String(); message != "" {
|
||||||
|
err = fmt.Errorf("%s", message)
|
||||||
|
}
|
||||||
|
done <- err
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer callback.Release()
|
||||||
|
fn.Invoke(append(args, callback)...)
|
||||||
|
return <-done
|
||||||
|
}
|
||||||
|
|
||||||
// currentSession 当前WebUI文档会话
|
// currentSession 当前WebUI文档会话
|
||||||
var currentSession *Session
|
var currentSession *Session
|
||||||
|
|
||||||
@@ -336,8 +346,8 @@ func exportDocument(args []js.Value) (any, error) {
|
|||||||
return nil, fmt.Errorf("ofd document is not opened")
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
}
|
}
|
||||||
renderer := currentSession.Renderer
|
renderer := currentSession.Renderer
|
||||||
renderer.OnExportProgress = func(completed, total int) {
|
renderer.OnExportProgress = func(completed, total int) error {
|
||||||
args[4].Invoke(completed, total)
|
return awaitExport(args[4], completed, total)
|
||||||
}
|
}
|
||||||
defer func() { renderer.OnExportProgress = nil }()
|
defer func() { renderer.OnExportProgress = nil }()
|
||||||
var indices []int
|
var indices []int
|
||||||
|
|||||||
+2
-2
@@ -24,13 +24,13 @@ import (
|
|||||||
// Renderer 渲染器实现
|
// Renderer 渲染器实现
|
||||||
// 通过NewRenderer创建,实例及共享的Reader需串行使用
|
// 通过NewRenderer创建,实例及共享的Reader需串行使用
|
||||||
// OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观
|
// OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观
|
||||||
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,不代表封装或写入成功
|
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
|
||||||
type Renderer struct {
|
type Renderer struct {
|
||||||
Reader *Reader
|
Reader *Reader
|
||||||
DPI float64
|
DPI float64
|
||||||
RenderAnnotations bool
|
RenderAnnotations bool
|
||||||
OnPageText func(*PageContent, *PageText)
|
OnPageText func(*PageContent, *PageText)
|
||||||
OnExportProgress func(completed, total int)
|
OnExportProgress func(completed, total int) error
|
||||||
DrawParams map[string]*DrawParam
|
DrawParams map[string]*DrawParam
|
||||||
CompositeGraphicUnits map[string]*CompositeGraphicUnit
|
CompositeGraphicUnits map[string]*CompositeGraphicUnit
|
||||||
fontFamily *canvas.FontFamily
|
fontFamily *canvas.FontFamily
|
||||||
|
|||||||
+23
-10
@@ -90,8 +90,8 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int)
|
|||||||
method = zip.Store
|
method = zip.Store
|
||||||
}
|
}
|
||||||
width := len(strconv.Itoa(count))
|
width := len(strconv.Itoa(count))
|
||||||
if r.OnExportProgress != nil {
|
if err := r.exportProgress(0, len(indices)); err != nil {
|
||||||
r.OnExportProgress(0, len(indices))
|
return err
|
||||||
}
|
}
|
||||||
for i, index := range indices {
|
for i, index := range indices {
|
||||||
page, err := r.Reader.PageContentByIndex(index)
|
page, err := r.Reader.PageContentByIndex(index)
|
||||||
@@ -108,13 +108,23 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int)
|
|||||||
if err := render(page, entry); err != nil {
|
if err := render(page, entry); err != nil {
|
||||||
return fmt.Errorf("failed to export page %d: %w", index+1, err)
|
return fmt.Errorf("failed to export page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
if r.OnExportProgress != nil {
|
if err := r.exportProgress(i+1, len(indices)); err != nil {
|
||||||
r.OnExportProgress(i+1, len(indices))
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return archive.Close()
|
return archive.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exportProgress 回报导出进度并传递调用方的停止原因
|
||||||
|
// 入参: completed 已处理页数, total 总页数
|
||||||
|
// 返回: error 停止原因
|
||||||
|
func (r *Renderer) exportProgress(completed, total int) error {
|
||||||
|
if r.OnExportProgress != nil {
|
||||||
|
return r.OnExportProgress(completed, total)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// rasterRenderer 保留页面物理尺寸的光栅渲染器
|
// rasterRenderer 保留页面物理尺寸的光栅渲染器
|
||||||
type rasterRenderer struct {
|
type rasterRenderer struct {
|
||||||
*rasterizer.Rasterizer
|
*rasterizer.Rasterizer
|
||||||
@@ -288,8 +298,8 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if r.OnExportProgress != nil {
|
if err := r.exportProgress(0, len(indices)); err != nil {
|
||||||
r.OnExportProgress(0, len(indices))
|
return err
|
||||||
}
|
}
|
||||||
pages := make([]pdfPage, len(indices))
|
pages := make([]pdfPage, len(indices))
|
||||||
for i, index := range indices {
|
for i, index := range indices {
|
||||||
@@ -313,8 +323,8 @@ func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) e
|
|||||||
if len(contents) == 0 {
|
if len(contents) == 0 {
|
||||||
return fmt.Errorf("no pages found")
|
return fmt.Errorf("no pages found")
|
||||||
}
|
}
|
||||||
if r.OnExportProgress != nil {
|
if err := r.exportProgress(0, len(contents)); err != nil {
|
||||||
r.OnExportProgress(0, len(contents))
|
return err
|
||||||
}
|
}
|
||||||
pages := make([]pdfPage, len(contents))
|
pages := make([]pdfPage, len(contents))
|
||||||
for i, page := range contents {
|
for i, page := range contents {
|
||||||
@@ -330,7 +340,7 @@ func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) e
|
|||||||
// renderPDFPages 渲染页面并复用调用方的字节缓冲
|
// renderPDFPages 渲染页面并复用调用方的字节缓冲
|
||||||
// 入参: pages 页面列表, writer 输出流, progress 页面处理进度
|
// 入参: pages 页面列表, writer 输出流, progress 页面处理进度
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (r *Renderer) renderPDFPages(pages []pdfPage, writer io.Writer, progress func(int, int)) error {
|
func (r *Renderer) renderPDFPages(pages []pdfPage, writer io.Writer, progress func(int, int) error) error {
|
||||||
navigation := newPDFNavigation(r, r.Reader.doc, pages)
|
navigation := newPDFNavigation(r, r.Reader.doc, pages)
|
||||||
buf, direct := writer.(*bytes.Buffer)
|
buf, direct := writer.(*bytes.Buffer)
|
||||||
if !direct {
|
if !direct {
|
||||||
@@ -354,7 +364,10 @@ func (r *Renderer) renderPDFPages(pages []pdfPage, writer io.Writer, progress fu
|
|||||||
return fmt.Errorf("failed to render page %d: %w", i+1, err)
|
return fmt.Errorf("failed to render page %d: %w", i+1, err)
|
||||||
}
|
}
|
||||||
if progress != nil {
|
if progress != nil {
|
||||||
progress(i+1, len(pages))
|
if err := progress(i+1, len(pages)); err != nil {
|
||||||
|
buf.Truncate(start)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := p.Close(); err != nil {
|
if err := p.Close(); err != nil {
|
||||||
|
|||||||
Reference in new issue
Block a user