diff --git a/proto/ipp/getdocument.go b/proto/ipp/getdocument.go new file mode 100644 index 0000000..e01d59f --- /dev/null +++ b/proto/ipp/getdocument.go @@ -0,0 +1,159 @@ +// MFP - Multi-Function Printers and scanners toolkit +// IPP - Internet Printing Protocol implementation +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Request and Response for Get-Next-Document-Data operation + +package ipp + +import ( + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/goipp" +) + +// GetNextDocumentDataRequest is the Get-Next-Document-Data request. +// +// The Get-Next-Document-Data operation allows a Scan +// Client to retrieve the scan data from an existing Job object, enabling +// pull scanning. The target Job MUST be in the 'processing' or 'completed' +// state. +type GetNextDocumentDataRequest struct { + ObjectRawAttrs + RequestHeader + + OperationGroup + + PrinterURI optional.Val[string] `ipp:"printer-uri"` + JobID optional.Val[int] `ipp:"job-id"` + RequestingUserName optional.Val[string] `ipp:"requesting-user-name"` + RequestingUserURI optional.Val[string] `ipp:"requesting-user-uri"` + DocumentDataWait optional.Val[bool] `ipp:"document-data-wait"` +} + +// GetNextDocumentDataResponse is the Get-Next-Document-Data response. +type GetNextDocumentDataResponse struct { + ObjectRawAttrs + ResponseHeader + + OperationGroup + + Compression optional.Val[KwCompression] `ipp:"compression"` + DocumentFormat optional.Val[string] `ipp:"document-format"` + DocumentDataGetInterval optional.Val[int] `ipp:"document-data-get-interval"` + LastDocument bool `ipp:"last-document"` + + Document *DocumentStatus +} + +type DocumentStatus struct { + ObjectRawAttrs + DocumentStatusGroup + + DocumentNumber optional.Val[int] `ipp:"document-number"` +} + +// DecodeDocumentStatusAttributes decodes [DocumentStatus] from +// [goipp.Attributes]. +func DecodeDocumentStatusAttributes(attrs goipp.Attributes, + opt *DecoderOptions) (*DocumentStatus, error) { + + doc := &DocumentStatus{} + dec := NewDecoder(opt) + defer dec.Free() + + err := dec.Decode(doc, attrs) + if err != nil { + return nil, err + } + return doc, nil +} + +// GetOp returns GetNextDocumentDataRequest IPP Operation code. +func (rq *GetNextDocumentDataRequest) GetOp() goipp.Op { + return goipp.OpGetNextDocumentData +} + +// Encode encodes GetNextDocumentDataRequest into the goipp.Message. +func (rq *GetNextDocumentDataRequest) Encode() *goipp.Message { + enc := ippEncoder{} + + groups := goipp.Groups{ + { + Tag: goipp.TagOperationGroup, + Attrs: enc.Encode(rq), + }, + } + + return goipp.NewMessageWithGroups( + rq.Version, goipp.Code(rq.GetOp()), + rq.RequestID, groups, + ) +} + +// Decode decodes GetNextDocumentDataRequest from goipp.Message. +func (rq *GetNextDocumentDataRequest) Decode( + msg *goipp.Message, opt *DecoderOptions) error { + + rq.Version = msg.Version + rq.RequestID = msg.RequestID + + dec := NewDecoder(opt) + defer dec.Free() + + err := dec.Decode(rq, msg.Operation) + if err != nil { + return err + } + + return nil +} + +// Encode encodes GetNextDocumentDataResponse into the goipp.Message. +func (rsp *GetNextDocumentDataResponse) Encode() *goipp.Message { + enc := ippEncoder{} + + groups := goipp.Groups{ + { + Tag: goipp.TagOperationGroup, + Attrs: enc.Encode(rsp), + }, + } + + if rsp.Document != nil { + groups = append(groups, goipp.Group{ + Tag: goipp.TagDocumentGroup, + Attrs: enc.Encode(rsp.Document), + }) + } + + return goipp.NewMessageWithGroups( + rsp.Version, goipp.Code(rsp.Status), + rsp.RequestID, groups, + ) +} + +// Decode decodes GetNextDocumentDataResponse from goipp.Message. +func (rsp *GetNextDocumentDataResponse) Decode( + msg *goipp.Message, opt *DecoderOptions) error { + + rsp.Version = msg.Version + rsp.RequestID = msg.RequestID + rsp.Status = goipp.Status(msg.Code) + + dec := NewDecoder(opt) + defer dec.Free() + + err := dec.Decode(rsp, msg.Operation) + if err != nil { + return err + } + + rsp.Document, err = DecodeDocumentStatusAttributes(msg.Document, opt) + if err != nil { + return err + } + + return nil +} diff --git a/proto/ipp/handler.go b/proto/ipp/handler.go index 4c842bc..d00a8d1 100644 --- a/proto/ipp/handler.go +++ b/proto/ipp/handler.go @@ -19,7 +19,7 @@ import ( type Handler struct { Op goipp.Op callback func(context.Context, *goipp.Message, io.Reader) ( - *goipp.Message, error) + *goipp.Message, io.ReadCloser, error) } // NewHandler creates a new IPP handler from the function that @@ -34,19 +34,19 @@ func NewHandler[RQT any, RQ interface { *RQT Request - }](f func(ctx context.Context, rq RQ) (*goipp.Message, error)) *Handler { + }](f func(ctx context.Context, rq RQ) (*goipp.Message, io.ReadCloser, error)) *Handler { callback := func(ctx context.Context, rqMsg *goipp.Message, body io.Reader) ( - *goipp.Message, error) { + *goipp.Message, io.ReadCloser, error) { rq := RQ(new(RQT)) rq.Header().setBody(body) err := rq.Decode(rqMsg, nil) if err != nil { - return nil, err + return nil, nil, err } return f(ctx, rq) @@ -60,6 +60,6 @@ func NewHandler[RQT any, // handle handles the received request. func (h *Handler) handle(ctx context.Context, rq *goipp.Message, body io.Reader) ( - *goipp.Message, error) { + *goipp.Message, io.ReadCloser, error) { return h.callback(ctx, rq, body) } diff --git a/proto/ipp/printer.go b/proto/ipp/printer.go index 87d0461..3deb04c 100644 --- a/proto/ipp/printer.go +++ b/proto/ipp/printer.go @@ -79,27 +79,27 @@ func (printer *Printer) ServeHTTP(w http.ResponseWriter, rq *http.Request) { // handleGetPrinterAttributes handles Get-Printer-Attributes request. func (printer *Printer) handleGetPrinterAttributes( ctx context.Context, - rq *GetPrinterAttributesRequest) (*goipp.Message, error) { + rq *GetPrinterAttributesRequest) (*goipp.Message, io.ReadCloser, error) { - return rq.Apply(printer.attrs, printer.options.UseRawPrinterAttributes), nil + return rq.Apply(printer.attrs, printer.options.UseRawPrinterAttributes), nil, nil } // handleValidateJob handles Validate-Job request. func (printer *Printer) handleValidateJob( ctx context.Context, - rq *ValidateJobRequest) (*goipp.Message, error) { + rq *ValidateJobRequest) (*goipp.Message, io.ReadCloser, error) { rsp := ValidateJobResponse{ ResponseHeader: rq.ResponseHeader(goipp.StatusOk), } - return rsp.Encode(), nil + return rsp.Encode(), nil, nil } // handleCreateJob handles Create-Job request. func (printer *Printer) handleCreateJob( ctx context.Context, - rq *CreateJobRequest) (*goipp.Message, error) { + rq *CreateJobRequest) (*goipp.Message, io.ReadCloser, error) { // Create new job j := newJob(&rq.JobCreateOperation, rq.Job) @@ -119,13 +119,13 @@ func (printer *Printer) handleCreateJob( }, } - return rsp.Encode(), nil + return rsp.Encode(), nil, nil } // handleCreateJob handles Send-Document request. func (printer *Printer) handleSendDocument( ctx context.Context, - rq *SendDocumentRequest) (*goipp.Message, error) { + rq *SendDocumentRequest) (*goipp.Message, io.ReadCloser, error) { // Lookup the job var j *job @@ -137,7 +137,7 @@ func (printer *Printer) handleSendDocument( err := NewErrIPPFromRequest(rq, goipp.StatusErrorNotFound, "job not found (job-id=%d)", *rq.JobID) - return nil, err + return nil, nil, err } case rq.JobURI != nil: @@ -146,14 +146,14 @@ func (printer *Printer) handleSendDocument( err := NewErrIPPFromRequest(rq, goipp.StatusErrorNotFound, "job not found (job-uri=%q)", *rq.JobURI) - return nil, err + return nil, nil, err } default: err := NewErrIPPFromRequest(rq, goipp.StatusErrorBadRequest, "missed job-id and job-uri attributes") - return nil, err + return nil, nil, err } j.Lock() @@ -164,14 +164,14 @@ func (printer *Printer) handleSendDocument( err := NewErrIPPFromRequest(rq, goipp.StatusErrorNotPossible, "job is not in pending-held state") - return nil, err + return nil, nil, err } if j.SendDocumentActive { err := NewErrIPPFromRequest(rq, goipp.StatusErrorNotPossible, "Send-Document already in progress") - return nil, err + return nil, nil, err } // Consume the document body @@ -231,5 +231,5 @@ func (printer *Printer) handleSendDocument( }, } - return rsp.Encode(), nil + return rsp.Encode(), nil, nil } diff --git a/proto/ipp/scanner.go b/proto/ipp/scanner.go index 5293cd7..be62157 100644 --- a/proto/ipp/scanner.go +++ b/proto/ipp/scanner.go @@ -10,13 +10,32 @@ package ipp import ( "context" + "io" "net/http" "sync" "github.com/OpenPrinting/go-mfp/abstract" + "github.com/OpenPrinting/go-mfp/util/optional" "github.com/OpenPrinting/goipp" ) +// docResult holds the outcome of a doc.Next() call delivered via channel. +type docResult struct { + file abstract.DocumentFile + err error +} + +type docBodyCloser struct { + file abstract.DocumentFile + onClose func() +} + +func (d *docBodyCloser) Read(p []byte) (int, error) { return d.file.Read(p) } +func (d *docBodyCloser) Close() error { + d.onClose() + return nil +} + // Scanner implements the IPP Scan Service as defined in PWG5100.17. type Scanner struct { options ScannerOptions @@ -24,9 +43,11 @@ type Scanner struct { attrs *PrinterAttributes q *queue - activeDoc abstract.Document - activeJob int - lock sync.Mutex + activeDoc abstract.Document + activeJob int + activeDocPageNum int + activeDocCh chan docResult + lock sync.Mutex } // ScannerOptions extends [ServerOptions] with scanner-specific parameters. @@ -40,6 +61,11 @@ type ScannerOptions struct { // attributes based on PrinterAttributes.RawAttrs instead of the // PrinterAttributes.Encode result. See [PrinterOptions] for details. UseRawPrinterAttributes bool + + // DocumentDataGetInterval is the number of seconds the scanner tells + // the client to wait before retrying Get-Next-Document-Data when no + // document data is immediately available. + DocumentDataGetInterval int } // NewScanner creates a new [Scanner], whose facilities and behavior @@ -52,15 +78,17 @@ func NewScanner(attrs *PrinterAttributes, options ScannerOptions) *Scanner { server := NewServer(options.ServerOptions) scanner := &Scanner{ - options: options, - server: server, - attrs: attrs, - q: newQueue(), + options: options, + server: server, + attrs: attrs, + q: newQueue(), + activeDocCh: make(chan docResult, 1), } // Install scan-service handlers. server.RegisterHandler(NewHandler(scanner.handleGetPrinterAttributes)) server.RegisterHandler(NewHandler(scanner.handleCreateScanJob)) + server.RegisterHandler(NewHandler(scanner.handleGetNextDocumentData)) return scanner } @@ -74,19 +102,19 @@ func (scanner *Scanner) ServeHTTP(w http.ResponseWriter, rq *http.Request) { // handleGetPrinterAttributes handles Get-Printer-Attributes request. func (scanner *Scanner) handleGetPrinterAttributes( ctx context.Context, - rq *GetPrinterAttributesRequest) (*goipp.Message, error) { + rq *GetPrinterAttributesRequest) (*goipp.Message, io.ReadCloser, error) { - return rq.Apply(scanner.attrs, scanner.options.UseRawPrinterAttributes), nil + return rq.Apply(scanner.attrs, scanner.options.UseRawPrinterAttributes), nil, nil } // handleCreateScanJob handles Create-Job request on the Scan Service func (scanner *Scanner) handleCreateScanJob( ctx context.Context, - rq *CreateJobRequest) (*goipp.Message, error) { + rq *CreateJobRequest) (*goipp.Message, io.ReadCloser, error) { // input-attributes MUST be supplied by the client. if rq.InputAttributes == nil { - return nil, NewErrIPPFromRequest(rq, + return nil, nil, NewErrIPPFromRequest(rq, goipp.StatusErrorBadRequest, "missing required input-attributes") } @@ -96,7 +124,7 @@ func (scanner *Scanner) handleCreateScanJob( req := rq.JobCreateOperation.ToAbstract() filled, err := caps.FillRequest(&req) if err != nil { - return nil, NewErrIPPFromRequest(rq, + return nil, nil, NewErrIPPFromRequest(rq, goipp.StatusErrorAttributesOrValues, "invalid scan parameters: %s", err) } @@ -105,7 +133,7 @@ func (scanner *Scanner) handleCreateScanJob( scanner.lock.Lock() if scanner.activeDoc != nil { scanner.lock.Unlock() - return nil, NewErrIPPFromRequest(rq, + return nil, nil, NewErrIPPFromRequest(rq, goipp.StatusErrorBusy, "scanner is busy with another job") } @@ -113,7 +141,7 @@ func (scanner *Scanner) handleCreateScanJob( doc, err := scanner.options.Scanner.Scan(ctx, *filled) if err != nil { scanner.lock.Unlock() - return nil, NewErrIPPFromRequest(rq, + return nil, nil, NewErrIPPFromRequest(rq, goipp.StatusErrorDevice, "scan failed: %s", err) } @@ -123,6 +151,11 @@ func (scanner *Scanner) handleCreateScanJob( scanner.activeDoc = doc scanner.activeJob = j.JobID + scanner.activeDocPageNum = 0 + go func() { + f, err := doc.Next() + scanner.activeDocCh <- docResult{file: f, err: err} + }() scanner.lock.Unlock() j.Lock() @@ -140,5 +173,96 @@ func (scanner *Scanner) handleCreateScanJob( }, } - return rsp.Encode(), nil + return rsp.Encode(), nil, nil +} + +// handleGetNextDocumentData handles Get-Next-Document-Data requests +func (scanner *Scanner) handleGetNextDocumentData( + ctx context.Context, + rq *GetNextDocumentDataRequest) (*goipp.Message, io.ReadCloser, error) { + + scanner.lock.Lock() + defer scanner.lock.Unlock() + + jobID := optional.Get(rq.JobID) + if jobID == 0 { + return nil, nil, NewErrIPPFromRequest(rq, + goipp.StatusErrorBadRequest, + "missing job-id") + } + + if jobID != scanner.activeJob { + return nil, nil, NewErrIPPFromRequest(rq, + goipp.StatusErrorNotFound, + "no active scan job") + } + + doc := scanner.activeDoc + + var result docResult + select { + case result = <-scanner.activeDocCh: + default: + rsp := GetNextDocumentDataResponse{ + ResponseHeader: rq.ResponseHeader(goipp.StatusOk), + DocumentDataGetInterval: optional.New( + scanner.documentDataGetInterval()), + } + return rsp.Encode(), nil, nil + } + + if result.err == io.EOF { + scanner.cleanupActiveScan() + rsp := GetNextDocumentDataResponse{ + ResponseHeader: rq.ResponseHeader(goipp.StatusOk), + LastDocument: true, + } + return rsp.Encode(), nil, nil + } + + if result.err != nil { + scanner.cleanupActiveScan() + return nil, nil, NewErrIPPFromRequest(rq, + goipp.StatusErrorDevice, + "scan error: %s", result.err) + } + + scanner.activeDocPageNum++ + docPageNum := scanner.activeDocPageNum + + body := &docBodyCloser{ + file: result.file, + onClose: func() { + go func() { + f, err := doc.Next() + scanner.activeDocCh <- docResult{file: f, err: err} + }() + }, + } + + rsp := GetNextDocumentDataResponse{ + ResponseHeader: rq.ResponseHeader(goipp.StatusOk), + LastDocument: false, + DocumentFormat: optional.New(result.file.Format()), + Document: &DocumentStatus{ + DocumentNumber: optional.New(docPageNum)}, + } + + return rsp.Encode(), body, nil +} + +func (scanner *Scanner) documentDataGetInterval() int { + if scanner.options.DocumentDataGetInterval > 0 { + return scanner.options.DocumentDataGetInterval + } + return 5 +} + +func (scanner *Scanner) cleanupActiveScan() { + if scanner.activeDoc != nil { + scanner.activeDoc.Close() + scanner.activeDoc = nil + } + scanner.activeJob = 0 + scanner.activeDocPageNum = 0 } diff --git a/proto/ipp/server.go b/proto/ipp/server.go index 342f75e..304b4bb 100644 --- a/proto/ipp/server.go +++ b/proto/ipp/server.go @@ -11,6 +11,7 @@ package ipp import ( "bytes" "fmt" + "io" "mime" "net/http" "net/http/httputil" @@ -146,7 +147,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, rq *http.Request) { } // Handle the message - rsp, err := handler.handle(ctx, msg, body) + rsp, rspBody, err := handler.handle(ctx, msg, body) if err != nil { s.httpError(query, err) return @@ -187,6 +188,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, rq *http.Request) { log.Error(ctx, "IPP error sending response: %s", err) } + if rspBody != nil { + if _, err = io.Copy(query, rspBody); err != nil { + log.Error(ctx, "IPP error sending response body: %s", err) + } + rspBody.Close() + } + query.Finish() }