package triplestore import ( "bytes" "context" "encoding/json" "io" "mime/multipart" "net/http" "time" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/tkw1536/pkglib/timex" ) type TriplestoreUserPayload struct { Password string `json:"password"` AppSettings TriplestoreUserAppSettings `json:"appSettings"` GrantedAuthorities []string `json:"grantedAuthorities"` } type TriplestoreUserAppSettings struct { DefaultInference bool `json:"DEFAULT_INFERENCE"` DefaultVisGraphSchema bool `json:"DEFAULT_VIS_GRAPH_SCHEMA"` DefaultSameas bool `json:"DEFAULT_SAMEAS"` IgnoreSharedQueries bool `json:"IGNORE_SHARED_QUERIES"` ExecuteCount bool `json:"EXECUTE_COUNT"` } // http.Client Timeout to be used for "trivial" triplestore operations. // This includes e.g. CRUDing a specific repo. const tsTrivialTimeout = time.Minute // RequestHeaders represent headers of a raw http request type RequestHeaders struct { Accept string ContentType string } func (rh *RequestHeaders) With(headers RequestHeaders) *RequestHeaders { // create new request headers and copy the old options var newHeaders RequestHeaders if rh != nil { newHeaders = *rh } // add the options if headers.Accept != "" { newHeaders.Accept = headers.Accept } if headers.ContentType != "" { newHeaders.ContentType = headers.ContentType } return &newHeaders } // DoRest performs a (raw) http request to the without a body. func (ts Triplestore) DoRest(ctx context.Context, timeout time.Duration, method, url string, headers *RequestHeaders) (*http.Response, error) { return ts.DoRestWithReader(ctx, timeout, method, url, headers, nil) } // DoRestWithForm performs a http request where the body are all bytes read from fieldvalue. func (ts Triplestore) DoRestWithForm(ctx context.Context, timeout time.Duration, method, url string, headers *RequestHeaders, fieldname string, fieldvalue io.Reader) (*http.Response, error) { var buffer bytes.Buffer // write the file to it writer := multipart.NewWriter(&buffer) { part, err := writer.CreateFormFile(fieldname, "filename.txt") if err != nil { return nil, err } io.Copy(part, fieldvalue) } writer.Close() // and sent the reader as the body return ts.DoRestWithReader(ctx, timeout, method, url, headers.With(RequestHeaders{ContentType: writer.FormDataContentType()}), &buffer) } // DoRestWithReader performs a http request where the body is copied from the given io.Reader. // The caller must ensure the reader is closed. func (ts Triplestore) DoRestWithMarshal(ctx context.Context, timeout time.Duration, method, url string, headers *RequestHeaders, body any) (*http.Response, error) { // encode into a buffer var buffer bytes.Buffer if err := json.NewEncoder(&buffer).Encode(body); err != nil { return nil, err } return ts.DoRestWithReader(ctx, timeout, method, url, headers.With(RequestHeaders{ContentType: "application/json"}), &buffer) } // DoRestWithReader performs a http request where the body is copied from the given io.Reader. // The caller must ensure the reader is closed. func (ts Triplestore) DoRestWithReader(ctx context.Context, timeout time.Duration, method string, url string, headers *RequestHeaders, body io.Reader) (*http.Response, error) { // create the request object client := &http.Client{ Timeout: timeout, Transport: &http.Transport{ DisableKeepAlives: true, }, } // create the request and authentication req, err := http.NewRequestWithContext(ctx, method, ts.BaseURL+url, body) if err != nil { return nil, err } req.SetBasicAuth(ts.Config.TS.AdminUsername, ts.Config.TS.AdminPassword) // add extra headers if headers != nil && headers.Accept != "" { req.Header.Set("Accept", headers.Accept) } if headers != nil && headers.ContentType != "" { req.Header.Set("Content-Type", headers.ContentType) } // and send it return client.Do(req) } // Wait waits for the connection to the Triplestore to succeed. // This is achieved using a polling strategy. func (ts Triplestore) Wait(ctx context.Context) error { return timex.TickUntilFunc(func(time.Time) bool { res, err := ts.DoRest(ctx, tsTrivialTimeout, http.MethodGet, "/rest/repositories", nil) zerolog.Ctx(ctx).Trace().Err(err).Msg("Triplestore wait") if err != nil { return false } defer res.Body.Close() return true }, ctx, ts.PollInterval) } // PurgeUser deletes the specified user from the triplestore. // When the user does not exist, returns no error. func (ts Triplestore) PurgeUser(ctx context.Context, user string) error { res, err := ts.DoRest(ctx, tsTrivialTimeout, http.MethodDelete, "/rest/security/users/"+user, nil) if err != nil { return err } defer res.Body.Close() if res.StatusCode != http.StatusNoContent && res.StatusCode != http.StatusNotFound { return errors.Errorf("Delete returned code %d", res.StatusCode) } return nil } // PurgeRepo deletes the specified repo from the triplestore. // When the repo does not exist, returns no error. func (ts Triplestore) PurgeRepo(ctx context.Context, repo string) error { res, err := ts.DoRest(ctx, tsTrivialTimeout, http.MethodDelete, "/rest/repositories/"+repo, nil) if err != nil { return err } defer res.Body.Close() if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotFound { return errors.Errorf("Delete returned code %d", res.StatusCode) } return nil } type Repository struct { ID string `json:"id"` Title string `json:"title"` URI string `json:"uri"` Type string `json:"type"` SesameType string `json:"sesameType"` Location string `json:"location"` Readable bool `json:"readable"` Writable bool `json:"writable"` Local bool `json:"local"` }